From f1f736cf3c4819bb60bfda6575bec8bc01d52c9e Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Thu, 22 Jun 2017 11:55:12 +0200 Subject: [PATCH 001/151] Mark the footprint as virtual in the pretty format The export currently adds (attr smd), which marks the footprint as an SMD component (which internally sets the MOD_CMS attribute, and in the GUI marks the component as "Normal+Insert"). This causes it to be exported in a .pos file for a pick & place machine. Since this is just a silkscreen and not an actual component, this makes no sense. This commit instead sets (attr virtual) (which internally sets MOD_VIRTUAL, and in the GUI marks the component as "Virtual") which causes it to be ignored by various parts of kicad that iterate over actual components. --- svg2mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod.py b/svg2mod.py index 21529f3..4866f8b 100755 --- a/svg2mod.py +++ b/svg2mod.py @@ -1201,7 +1201,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( From 75d59f297846c398f3c32d5b0d675c9e08d2bbd2 Mon Sep 17 00:00:00 2001 From: Farsheed Date: Wed, 9 May 2018 12:45:17 -0500 Subject: [PATCH 002/151] Update svg2mod.py to work with python3 methods --- svg2mod/svg2mod.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index cc3f060..0428fc8 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -553,7 +553,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 +564,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 ) ) @@ -653,7 +653,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 @@ -1094,7 +1094,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 +1111,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: From a028e419ffa4976b8babcfcf01c5e2248f82cc33 Mon Sep 17 00:00:00 2001 From: Farsheed Date: Wed, 9 May 2018 12:46:27 -0500 Subject: [PATCH 003/151] Updates svg.py to work with python3 --- svg2mod/svg/svg/svg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 244fdaa..02949d2 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -262,7 +262,7 @@ def __init__(self, elt=None): 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": From 57e62dff045832f4adf012445814f4b04d02882e Mon Sep 17 00:00:00 2001 From: Farsheed Date: Wed, 9 May 2018 12:49:04 -0500 Subject: [PATCH 004/151] Update README.md Add notes about python3 --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index d048ea5..37e7c09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,19 @@ # 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. +## Requirements + +Python 3 + +## Installation + +```python3 setup.py install``` + +- OR - + +```pip3 install git+https://github.com/zirafa/svg2mod``` + + ## Usage ``` usage: svg2mod.py [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] From 231ecdd7683034b112d6f83c834cbb01a94506e3 Mon Sep 17 00:00:00 2001 From: Farsheed Date: Wed, 9 May 2018 12:52:54 -0500 Subject: [PATCH 005/151] Update README.md --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 37e7c09..1777191 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,20 @@ Python 3 ```python3 setup.py install``` -- OR - +OR you can install it using the PIP package manager: ```pip3 install git+https://github.com/zirafa/svg2mod``` +## Example + +```python3 svg2mod.py -i input.svg``` + +OR for PIP + +```svg2mod -i input.svg``` + + ## Usage ``` usage: svg2mod.py [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] From 32977b8ffdc7a1269bd9656c69c0d738de24d002 Mon Sep 17 00:00:00 2001 From: Farsheed Date: Wed, 9 May 2018 12:56:21 -0500 Subject: [PATCH 006/151] Update README.md --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 1777191..9ba8afc 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,14 @@ Python 3 ## Installation -```python3 setup.py install``` - -OR you can install it using the PIP package manager: - ```pip3 install git+https://github.com/zirafa/svg2mod``` +Note: ```python3 setup.py install``` does not work. ## Example -```python3 svg2mod.py -i input.svg``` - -OR for PIP - ```svg2mod -i input.svg``` - ## Usage ``` usage: svg2mod.py [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] From ac6714bf25281bf12317c23045dfc39236839cf0 Mon Sep 17 00:00:00 2001 From: Farsheed Date: Wed, 16 May 2018 16:04:27 -0500 Subject: [PATCH 007/151] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9ba8afc..930d91e 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,11 @@ Note: ```python3 setup.py install``` does not work. ## Example -```svg2mod -i input.svg``` +```svg2mod -i input.svg -p 1.0``` ## Usage ``` -usage: svg2mod.py [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] +usage: svg2mod [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] [-f FACTOR] [-p PRECISION] [-d DPI] [--front-only] [--format FORMAT] [--units UNITS] From ecfcabe763ac1e9b7fa14eedfc5a4b9b33c44223 Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 16:52:03 -0600 Subject: [PATCH 008/151] Update .gitignore to block more files and initial changes to svg2mod --- .gitignore | 4 ++++ svg2mod/svg2mod.py | 36 ++++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 16 deletions(-) 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/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index cc3f060..b0d8451 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -535,13 +535,13 @@ 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 + ## 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, + 0.0, + 0.0, ) @@ -1176,13 +1176,20 @@ class Svg2ModExportPretty( Svg2ModExport ): layer_map = { #'inkscape-name' : kicad-name, - 'Cu' : "{}.Cu", - 'Adhes' : "{}.Adhes", - 'Paste' : "{}.Paste", - 'SilkS' : "{}.SilkS", - 'Mask' : "{}.Mask", - 'CrtYd' : "{}.CrtYd", - 'Fab' : "{}.Fab", + '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", + 'F.CrtYd' : "F.CrtYd", + 'B.CrtYd' : "B.CrtYd", + 'F.Fab' : "F.Fab", + 'B.Fab' : "B.Fab", 'Edge.Cuts' : "Edge.Cuts" } @@ -1191,10 +1198,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 ] #------------------------------------------------------------------------ From 576f3b7830e55f3272ad24f79bc0bb06e89f1d6a Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 17:04:55 -0600 Subject: [PATCH 009/151] Update legacy to support layers and update readme --- README.md | 35 ++++++++++++++++++++--------------- svg2mod/svg2mod.py | 15 ++++++++++----- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d048ea5..bb8d486 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,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 +48,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/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index b0d8451..d9dd673 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -743,11 +743,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 ], From fad7bc3773a76e0bca12b5b8d09c1c16a75c5bf8 Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 17:57:57 -0600 Subject: [PATCH 010/151] Added centering as an option and remove front only option --- svg2mod/svg2mod.py | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index d9dd673..3d32e4d 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: @@ -509,6 +510,7 @@ def __init__( self, svg2mod_import, file_name, + center, scale_factor = 1.0, precision = 20.0, use_mm = True, @@ -524,6 +526,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 +538,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, - 0.0, - ) + self.translation = svg.Point( + 0.0 - adjust_x, + 0.0 - adjust_y, + ) + else: + self.translation = svg.Point( + 0.0, + 0.0, + ) #------------------------------------------------------------------------ @@ -767,22 +776,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 #------------------------------------------------------------------------ @@ -958,6 +968,7 @@ def __init__( self, svg2mod_import, file_name, + center, scale_factor = 1.0, precision = 20.0, dpi = DEFAULT_DPI, @@ -969,11 +980,11 @@ def __init__( super( Svg2ModExportLegacyUpdater, self ).__init__( svg2mod_import, file_name, + center, scale_factor, precision, use_mm, dpi, - include_reverse, ) @@ -1449,6 +1460,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 From 8b912e133749562043ccdbc62732369395915ebe Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 18:00:53 -0600 Subject: [PATCH 011/151] Fix README.md --- README.md | 9 +++++---- svg2mod/svg2mod.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bb8d486..01d2f54 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ This is a small program to convert Inkscape SVG drawings to KiCad footprint modu ## 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] [--front-only] [--format FORMAT] + [--units UNITS] [-d DPI] [--center] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -24,10 +24,11 @@ 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 diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 3d32e4d..35168ea 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1480,4 +1480,3 @@ def get_arguments(): #---------------------------------------------------------------------------- -# vi: set et sts=4 sw=4 ts=4: From c8792070587e335a433a8c1e66df8e5c1db8e897 Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 18:04:11 -0600 Subject: [PATCH 012/151] Remove null flags --- README.md | 5 ++--- svg2mod/svg2mod.py | 9 --------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 01d2f54..69f2113 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ This is a small program to convert Inkscape SVG drawings to KiCad footprint modu ## Usage ``` usage: svg2mod [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] - [-f FACTOR] [-p PRECISION] [--front-only] [--format FORMAT] - [--units UNITS] [-d DPI] [--center] + [-f FACTOR] [-p PRECISION] [--format FORMAT] [--units UNITS] + [-d DPI] [--center] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -24,7 +24,6 @@ optional arguments: -p PRECISION, --precision PRECISION smoothness for approximating curves with line segments (float) - --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) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 35168ea..46f3806 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1422,15 +1422,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, From 733774554803db52c4151fb7b98bedcab0c23e1e Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 18:18:41 -0600 Subject: [PATCH 013/151] Add more options for pretty format --- svg2mod/svg2mod.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 46f3806..c480d36 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1202,11 +1202,15 @@ class Svg2ModExportPretty( Svg2ModExport ): '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", - 'Edge.Cuts' : "Edge.Cuts" + 'B.Fab' : "B.Fab" } From 8115775a7cbd5823a43e553b756fcd04b741aff6 Mon Sep 17 00:00:00 2001 From: Michael Julander <1110mdj@gmail.com> Date: Thu, 23 Aug 2018 13:16:53 -0600 Subject: [PATCH 014/151] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69f2113..7318059 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 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. +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. ## Usage ``` From fae2216436e3fd5ef278d5449185fbda873cfb6f Mon Sep 17 00:00:00 2001 From: Michael Julander <1110mdj@gmail.com> Date: Thu, 23 Aug 2018 14:20:35 -0600 Subject: [PATCH 015/151] Changed the model attribute to virtual The export currently adds (attr smd), which marks the footprint as an SMD component (which internally sets the MOD_CMS attribute, and in the GUI marks the component as "Normal+Insert"). This causes it to be exported in a .pos file for a pick & place machine. Since this is just a silkscreen and not an actual component, this makes no sense. This commit instead sets (attr virtual) (which internally sets MOD_VIRTUAL, and in the GUI marks the component as "Virtual") which causes it to be ignored by various parts of kicad that iterate over actual components. --- svg2mod/svg2mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index c480d36..52d5c96 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1233,7 +1233,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( From 5a6b18bbf6b1413eadcb48db2ac72ab9b2d79c34 Mon Sep 17 00:00:00 2001 From: Mark Rages Date: Thu, 14 Feb 2019 22:56:42 -0700 Subject: [PATCH 016/151] Fix closepath per https://github.com/cjlano/svg/pull/12 --- svg2mod/svg/svg/svg.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 244fdaa..e5f3a58 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -258,10 +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(): id = self.parse_name( id ) @@ -401,7 +401,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 +708,3 @@ def default(self, obj): tag = getattr(cls, 'tag', None) if tag: svgClass[svg_ns + tag] = cls - From 5de072762f7c485a3f22f8fff7a4bba6d2eb0ad8 Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 16:52:03 -0600 Subject: [PATCH 017/151] Update .gitignore to block more files and initial changes to svg2mod --- .gitignore | 4 ++++ svg2mod/svg2mod.py | 36 ++++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 16 deletions(-) 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/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index f95c068..51a87c6 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -535,13 +535,13 @@ 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 + ## 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, + 0.0, + 0.0, ) @@ -1176,13 +1176,20 @@ class Svg2ModExportPretty( Svg2ModExport ): layer_map = { #'inkscape-name' : kicad-name, - 'Cu' : "{}.Cu", - 'Adhes' : "{}.Adhes", - 'Paste' : "{}.Paste", - 'SilkS' : "{}.SilkS", - 'Mask' : "{}.Mask", - 'CrtYd' : "{}.CrtYd", - 'Fab' : "{}.Fab", + '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", + 'F.CrtYd' : "F.CrtYd", + 'B.CrtYd' : "B.CrtYd", + 'F.Fab' : "F.Fab", + 'B.Fab' : "B.Fab", 'Edge.Cuts' : "Edge.Cuts" } @@ -1191,10 +1198,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 ] #------------------------------------------------------------------------ From 389af93f2fa514ac86a364475504246efd1d56d7 Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 17:04:55 -0600 Subject: [PATCH 018/151] Update legacy to support layers and update readme --- README.md | 35 ++++++++++++++++++++--------------- svg2mod/svg2mod.py | 15 ++++++++++----- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d048ea5..bb8d486 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,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 +48,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/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 51a87c6..a7edb3a 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -743,11 +743,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 ], From da01c78b0d5c1ba11628340f65570f66f368b8c1 Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 17:57:57 -0600 Subject: [PATCH 019/151] Added centering as an option and remove front only option --- svg2mod/svg2mod.py | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index a7edb3a..d9be704 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: @@ -509,6 +510,7 @@ def __init__( self, svg2mod_import, file_name, + center, scale_factor = 1.0, precision = 20.0, use_mm = True, @@ -524,6 +526,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 +538,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, - 0.0, - ) + self.translation = svg.Point( + 0.0 - adjust_x, + 0.0 - adjust_y, + ) + else: + self.translation = svg.Point( + 0.0, + 0.0, + ) #------------------------------------------------------------------------ @@ -767,22 +776,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 #------------------------------------------------------------------------ @@ -958,6 +968,7 @@ def __init__( self, svg2mod_import, file_name, + center, scale_factor = 1.0, precision = 20.0, dpi = DEFAULT_DPI, @@ -969,11 +980,11 @@ def __init__( super( Svg2ModExportLegacyUpdater, self ).__init__( svg2mod_import, file_name, + center, scale_factor, precision, use_mm, dpi, - include_reverse, ) @@ -1449,6 +1460,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 From 5fc8d98e2241f2789eb389aa6e75e4caee7be64a Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 18:00:53 -0600 Subject: [PATCH 020/151] Fix README.md --- README.md | 9 +++++---- svg2mod/svg2mod.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bb8d486..01d2f54 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ This is a small program to convert Inkscape SVG drawings to KiCad footprint modu ## 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] [--front-only] [--format FORMAT] + [--units UNITS] [-d DPI] [--center] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -24,10 +24,11 @@ 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 diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index d9be704..30cda21 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1480,4 +1480,3 @@ def get_arguments(): #---------------------------------------------------------------------------- -# vi: set et sts=4 sw=4 ts=4: From 78a2857ab9fdcddff6f6ccc6b8a67269e3f85fee Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 18:04:11 -0600 Subject: [PATCH 021/151] Remove null flags --- README.md | 5 ++--- svg2mod/svg2mod.py | 9 --------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 01d2f54..69f2113 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ This is a small program to convert Inkscape SVG drawings to KiCad footprint modu ## Usage ``` usage: svg2mod [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] - [-f FACTOR] [-p PRECISION] [--front-only] [--format FORMAT] - [--units UNITS] [-d DPI] [--center] + [-f FACTOR] [-p PRECISION] [--format FORMAT] [--units UNITS] + [-d DPI] [--center] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -24,7 +24,6 @@ optional arguments: -p PRECISION, --precision PRECISION smoothness for approximating curves with line segments (float) - --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) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 30cda21..031a0fe 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1422,15 +1422,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, From 93b978cbcd2b9f007f4cbe96c33f4bfc04d396cd Mon Sep 17 00:00:00 2001 From: NaH012 <1110mdj@gmail.com> Date: Mon, 13 Aug 2018 18:18:41 -0600 Subject: [PATCH 022/151] Add more options for pretty format --- svg2mod/svg2mod.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 031a0fe..52d5c96 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1202,11 +1202,15 @@ class Svg2ModExportPretty( Svg2ModExport ): '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", - 'Edge.Cuts' : "Edge.Cuts" + 'B.Fab' : "B.Fab" } From 0f40b47f4fdd70aaedfbcfdd30821f91afc18115 Mon Sep 17 00:00:00 2001 From: Michael Julander <1110mdj@gmail.com> Date: Thu, 23 Aug 2018 13:16:53 -0600 Subject: [PATCH 023/151] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69f2113..7318059 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 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. +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. ## Usage ``` From 0c4627afe54040f647a003a9c6d9cd2dfaa392b4 Mon Sep 17 00:00:00 2001 From: Hector Martin Date: Sat, 1 Dec 2018 13:21:13 +0900 Subject: [PATCH 024/151] Do not close non-filled polygons --- svg2mod/svg2mod.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index b5ff31d..26a11e6 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -394,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: @@ -417,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( @@ -603,8 +604,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 : ] ) @@ -612,8 +615,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 From c162c8e00cfcb1fd107a10ff8063e1c7fa0760c6 Mon Sep 17 00:00:00 2001 From: Hector Martin Date: Mon, 3 Dec 2018 23:26:00 +0900 Subject: [PATCH 025/151] Fix stroke-width calculation for non-px units --- svg2mod/svg2mod.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 26a11e6..8a993a1 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -493,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 From 3b416846ce8dbda0e370f9ce49129a74d0bf23bf Mon Sep 17 00:00:00 2001 From: Terje Tjervaag Date: Fri, 28 Feb 2020 20:39:15 +0100 Subject: [PATCH 026/151] Remove empty style values --- svg2mod/svg2mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 8a993a1..2e11764 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -480,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() From 36853c964157705eade505ca277f90c5dabb54cc Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 10 Mar 2020 10:07:37 -0600 Subject: [PATCH 027/151] Merge terje/svg2mod into master --- README.md | 14 ++++++++++++++ svg2mod/svg/svg/svg.py | 2 +- svg2mod/svg2mod.py | 36 ++++++++++++++++++++---------------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 7318059..32d56c4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ # 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/zirafa/svg2mod``` + +Note: ```python3 setup.py install``` does not work. + +## Example + +```svg2mod -i input.svg -p 1.0``` + ## Usage ``` usage: svg2mod [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 244fdaa..02949d2 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -262,7 +262,7 @@ def __init__(self, elt=None): 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": diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 52d5c96..2e11764 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -394,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: @@ -417,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( @@ -479,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() @@ -492,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 @@ -562,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 @@ -573,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 ) ) @@ -603,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 : ] ) @@ -612,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 @@ -662,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 @@ -1110,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" ) @@ -1127,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: From 29f3d63a024acfdf24a71b3a17776b2b1cd11599 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 10 Mar 2020 10:19:17 -0600 Subject: [PATCH 028/151] Update pip instructions --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index b80a432..3bb276c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Python 3 ## Installation -```pip3 install git+https://github.com/zirafa/svg2mod``` +```pip3 install git+https://github.com/Sodium-Hydrogen/svg2mod``` Note: ```python3 setup.py install``` does not work. @@ -18,13 +18,8 @@ Note: ```python3 setup.py install``` does not work. ## Usage ``` usage: svg2mod [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] -<<<<<<< HEAD [-f FACTOR] [-p PRECISION] [--format FORMAT] [--units UNITS] [-d DPI] [--center] -======= - [-f FACTOR] [-p PRECISION] [-d DPI] [--front-only] [--format FORMAT] - [--units UNITS] ->>>>>>> 3b416846ce8dbda0e370f9ce49129a74d0bf23bf Convert Inkscape SVG drawings to KiCad footprint modules. From a98b69416208b2c8a1d8fee30c4495e7a2363536 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 10 Mar 2020 14:48:38 -0600 Subject: [PATCH 029/151] travis ci testing --- .travis.yml | 2 ++ setup.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..39bcfdf --- /dev/null +++ b/.travis.yml @@ -0,0 +1,2 @@ +language: python +script: pip3 install ./ 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'}, From cd2616d28030b5c534ea4864617cd0bcf4bb5a65 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 10 Mar 2020 14:54:20 -0600 Subject: [PATCH 030/151] Add a sample test --- .travis.yml | 2 +- README.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 39bcfdf..9f1311a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,2 +1,2 @@ language: python -script: pip3 install ./ +script: pip3 install ./ && svg2mod -i examples/dt-logo.svg diff --git a/README.md b/README.md index 3bb276c..70688e4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # svg2mod +[![Build Status](https://travis-ci.org/Sodium-Hydrogen/svg2mod.svg?branch=travis-testing)](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 From b9d5690c2f2795b02bff5b8884719f8f35c91e38 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 10 Mar 2020 15:00:06 -0600 Subject: [PATCH 031/151] Better test --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9f1311a..ed3b24d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,2 +1,2 @@ language: python -script: pip3 install ./ && svg2mod -i examples/dt-logo.svg +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 From 4b4ea8945c43e1f2e1aba08e8cc8312470838ba7 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 10 Mar 2020 15:12:46 -0600 Subject: [PATCH 032/151] Update Readme.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 70688e4..28e1749 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # svg2mod -[![Build Status](https://travis-ci.org/Sodium-Hydrogen/svg2mod.svg?branch=travis-testing)](https://travis-ci.org/Sodium-Hydrogen/svg2mod) +[![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. @@ -11,7 +11,7 @@ Python 3 ```pip3 install git+https://github.com/Sodium-Hydrogen/svg2mod``` -Note: ```python3 setup.py install``` does not work. +If building fails make sure setuptools is up to date. `pip3 install setuptools --upgrade` ## Example From 151cd9fd16e336f73f784e4fc32f614e60d72ad7 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 10 Mar 2020 15:37:30 -0600 Subject: [PATCH 033/151] Fix problem when merging --- svg2mod/svg/svg/svg.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 3f26c85..55c3da2 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -261,13 +261,7 @@ def __init__(self, elt=None): self.name = "" if elt is not None: -<<<<<<< HEAD - for id, value in elt.attrib.items(): -======= - - for id, value in elt.attrib.iteritems(): ->>>>>>> 5a6b18bbf6b1413eadcb48db2ac72ab9b2d79c34 id = self.parse_name( id ) if id[ "name" ] == "label": From 06ce17ef660aed49028da96e8501b9173840cb1a Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Thu, 12 Mar 2020 01:22:01 -0600 Subject: [PATCH 034/151] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28e1749..be59db4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # svg2mod -[![Build Status](https://travis-ci.org/Sodium-Hydrogen/svg2mod.svg?branch=master)](https://travis-ci.org/Sodium-Hydrogen/svg2mod) +[![Build Status](https://travis-ci.org/svg2mod/svg2mod.svg?branch=master)](https://travis-ci.org/svg2mod/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. From 5b091f811e34762d5f80bd87e7756c0479b8ca36 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Thu, 12 Mar 2020 07:44:49 -0600 Subject: [PATCH 035/151] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index be59db4..d521fba 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Python 3 ## Installation -```pip3 install git+https://github.com/Sodium-Hydrogen/svg2mod``` +```pip3 install git+https://github.com/svg2mod/svg2mod``` If building fails make sure setuptools is up to date. `pip3 install setuptools --upgrade` From ef32ae595ccaea8a75caa7524d63118ab74ff40c Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Thu, 12 Mar 2020 08:13:05 -0600 Subject: [PATCH 036/151] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d521fba..cfbc7a4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # svg2mod [![Build Status](https://travis-ci.org/svg2mod/svg2mod.svg?branch=master)](https://travis-ci.org/svg2mod/svg2mod) +__[@mtl](https://github.com/mtl) is no longer active. [https://github.com/svg2mod/svg2mod](https://github.com/svg2mod/svg2mod) is now the maintained branch.__ + 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 From ab1eb8d34cb7086a757262bdc61e727fda5f4000 Mon Sep 17 00:00:00 2001 From: javl Date: Wed, 6 May 2020 21:14:04 +0200 Subject: [PATCH 037/151] upgrade shebang to python3, add __main__ check when ran --- svg2mod/svg2mod.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 2e11764..5253143 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1,6 +1,4 @@ -#!/usr/bin/python - -from __future__ import absolute_import +#!/usr/bin/env python3 import argparse import datetime @@ -1457,8 +1455,8 @@ def get_arguments(): metavar = 'DPI', help = "DPI of the SVG file (int)", default = DEFAULT_DPI, - ) - + ) + parser.add_argument( '--center', dest = 'center', @@ -1474,8 +1472,8 @@ def get_arguments(): #------------------------------------------------------------------------ #---------------------------------------------------------------------------- - -main() +if __name__ == "__main__": + main() #---------------------------------------------------------------------------- From 747fea83366bdd01947489641e0a812697613c24 Mon Sep 17 00:00:00 2001 From: Giulio Moro Date: Thu, 8 Oct 2020 17:46:28 +0100 Subject: [PATCH 038/151] Add option to skip hidden layers --- svg2mod/svg/svg/svg.py | 4 ++++ svg2mod/svg2mod.py | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 55c3da2..ad155d5 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -260,12 +260,16 @@ def __init__(self, elt=None): Transformable.__init__(self, elt) self.name = "" + self.hidden = False if elt is not None: for id, value in elt.attrib.items(): id = self.parse_name( id ) if id[ "name" ] == "label": self.name = value + if id[ "name" ] == "style": + if re.search( "display\s*:\s*none", value ): + self.hidden = True @staticmethod def parse_name( tag ): diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 5253143..63ee64d 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -35,7 +35,8 @@ def main(): imported = Svg2ModImport( args.input_file_name, args.module_name, - args.module_value + args.module_value, + args.ignore_hidden_layers, ) # Pick an output file name if none was provided: @@ -438,7 +439,27 @@ class Svg2ModImport( object ): #------------------------------------------------------------------------ - def __init__( self, file_name, module_name, module_value ): + def _prune_hidden( self, items = None ): + + if items is None: + + items = self.svg.items + self.svg.items = [] + + for item in items: + + if not isinstance( item, svg.Group ): + continue + + if( item.hidden ): + print("Ignoring hidden SVG layer: {}".format( item.name ) ) + else: + self.svg.items.append( item ) + + if(item.items): + self._prune_hidden( item.items ) + + def __init__( self, file_name, module_name, module_value, ignore_hidden_layers ): self.file_name = file_name self.module_name = module_name @@ -446,6 +467,8 @@ def __init__( self, file_name, module_name, module_value ): print( "Parsing SVG..." ) self.svg = svg.parse( file_name ) + if( ignore_hidden_layers ): + self._prune_hidden() #------------------------------------------------------------------------ @@ -579,6 +602,7 @@ def _prune( self, items = None ): #if re.search( name, item.name, re.I ): if name == item.name: print( "Found SVG layer: {}".format( item.name ) ) + self.imported.svg.items.append( item ) self.layers[ name ] = item break @@ -1466,6 +1490,15 @@ def get_arguments(): default = False, ) + parser.add_argument( + '-x', + dest = 'ignore_hidden_layers', + action = 'store_const', + const = True, + help = "Do not export hidden layers", + default = False, + ) + return parser.parse_args(), parser From 1666adcfdced3cc27056ba921c26be199b0eb107 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Fri, 9 Oct 2020 13:44:32 -0600 Subject: [PATCH 039/151] Better svg example to work with --- examples/dt-logo.mod | 500 ------------------------------------------- examples/dt-logo.svg | 162 -------------- examples/svg2mod.svg | 160 ++++++++++++++ 3 files changed, 160 insertions(+), 662 deletions(-) delete mode 100644 examples/dt-logo.mod delete mode 100644 examples/dt-logo.svg create mode 100644 examples/svg2mod.svg diff --git a/examples/dt-logo.mod b/examples/dt-logo.mod deleted file mode 100644 index ca01a35..0000000 --- a/examples/dt-logo.mod +++ /dev/null @@ -1,500 +0,0 @@ -PCBNEW-LibModule-V1 Wed 23 Sep 2015 12:56:11 PM -Units mm -$INDEX -DT-Logo-Front -DT-Logo-Back -$EndINDEX -# -# dt-logo.svg -# -$MODULE DT-Logo-Front -Po 0 0 0 15 00000000 00000000 ~~ -Li DT-Logo-Front -T0 0 -4.48650007222 1.524 1.524 0 0.3048 N I 21 "DT-Logo-Front" -T1 0 4.48650007222 1.524 1.524 0 0.3048 N I 21 "G***" -DP 0 0 0 0 45 0.00254 23 -Dl -1.11500002778 -1.43850007222 -Dl 1.11500002778 -1.43850007222 -Dl 1.21826499778 -1.43021840222 -Dl 1.31612738778 -1.40623191222 -Dl 1.40729941778 -1.36782838222 -Dl 1.49049330778 -1.31629559222 -Dl 1.56442127778 -1.25292132222 -Dl 1.62779554778 -1.17899335222 -Dl 1.67932833778 -1.09579946222 -Dl 1.71773186778 -1.00462743222 -Dl 1.74171835778 -0.906765042222 -Dl 1.75000002778 -0.803500072222 -Dl 1.75000002778 0.803499987556 -Dl 1.74171835778 0.906764959926 -Dl 1.71773186778 1.00462735636 -Dl 1.67932833778 1.09579939584 -Dl 1.62779554778 1.17899329736 -Dl 1.56442127778 1.25292127989 -Dl 1.49049330778 1.31629556242 -Dl 1.40729941778 1.36782836393 -Dl 1.31612738778 1.40623190342 -Dl 1.21826499778 1.43021839985 -Dl 1.11500002778 1.43850007222 -Dl -1.11500002778 1.43850007222 -Dl -1.21826499778 1.43021839985 -Dl -1.31612738778 1.40623190342 -Dl -1.40729941778 1.36782836393 -Dl -1.49049330778 1.31629556242 -Dl -1.56442127778 1.25292127989 -Dl -1.62779554778 1.17899329736 -Dl -1.67932833778 1.09579939584 -Dl -1.71773186778 1.00462735636 -Dl -1.74171835778 0.906764959926 -Dl -1.75000002778 0.803499987556 -Dl -1.75000002778 -0.803500072222 -Dl -1.74171835778 -0.906765042222 -Dl -1.71773186778 -1.00462743222 -Dl -1.67932833778 -1.09579946222 -Dl -1.62779554778 -1.17899335222 -Dl -1.56442127778 -1.25292132222 -Dl -1.49049330778 -1.31629559222 -Dl -1.40729941778 -1.36782838222 -Dl -1.31612738778 -1.40623191222 -Dl -1.21826499778 -1.43021840222 -Dl -1.11500002778 -1.43850007222 -DP 0 0 0 0 45 0.00254 15 -Dl -0.799826470222 -0.551812572889 -Dl -0.799826470222 0.551656560444 -Dl -0.565762111778 0.551656560444 -Dl -0.561958169057 0.551588736518 -Dl -0.557617048171 0.551401158364 -Dl -0.552923190034 0.551117666424 -Dl -0.548061035561 0.550762101138 -Dl -0.543215025667 0.550358302944 -Dl -0.538569601266 0.549930112284 -Dl -0.534309203273 0.549501369598 -Dl -0.530618272603 0.549095915324 -Dl -0.52768125017 0.548737589904 -Dl -0.525682576889 0.548450233778 -Dl -0.492995048016 0.540568430728 -Dl -0.462283746331 0.52830719253 -Dl -0.433893631435 0.512020431606 -Dl -0.40816966293 0.49206206038 -Dl -0.385456800417 0.468785991278 -Dl -0.366100003497 0.442546136722 -Dl -0.350444231772 0.413696409136 -Dl -0.338834444843 0.382590720946 -Dl -0.331615602311 0.349582984574 -Dl -0.329132663778 0.315027112444 -Dl -0.329132663778 -0.315022850889 -Dl -0.331662301481 -0.349341511769 -Dl -0.339000486336 -0.381745179104 -Dl -0.350771125962 -0.412034607335 -Dl -0.366598127977 -0.440010550903 -Dl -0.3861054 -0.46547376425 -Dl -0.40891684965 -0.488225001817 -Dl -0.434656384545 -0.508065018045 -Dl -0.462947912304 -0.524794567376 -Dl -0.493415340546 -0.538214404251 -Dl -0.525682576889 -0.548125283111 -Dl -0.528861655226 -0.548802540244 -Dl -0.532532627931 -0.549415840178 -Dl -0.536580107545 -0.549963455711 -Dl -0.54088870661 -0.550443659644 -Dl -0.545343037667 -0.550854724778 -Dl -0.549827713257 -0.551194923911 -Dl -0.554227345922 -0.551462529844 -Dl -0.558426548203 -0.551655815378 -Dl -0.562309932641 -0.551773053311 -Dl -0.565762111778 -0.551812516444 -Dl -0.799826470222 -0.551812572889 -DP 0 0 0 0 75 0.00254 15 -Dl -1.11853875578 -1.0274762681 -Dl -1.13990576822 -1.02618187506 -Dl -1.16015910005 -1.02243221638 -Dl -1.17903130383 -1.01642757278 -Dl -1.19625493213 -1.00836822494 -Dl -1.2115625375 -0.998454453586 -Dl -1.22468667251 -0.9868865394 -Dl -1.23535988973 -0.973864763088 -Dl -1.24331474171 -0.959589405351 -Dl -1.24828378102 -0.944260746888 -Dl -1.24999956022 -0.9280790684 -Dl -1.24999956022 0.927762714222 -Dl -1.24828378102 0.943953377266 -Dl -1.24331474171 0.959306407746 -Dl -1.23535988973 0.973617677884 -Dl -1.22468667251 0.986683059904 -Dl -1.2115625375 0.998298426028 -Dl -1.19625493213 1.00825964848 -Dl -1.17903130383 1.01636259948 -Dl -1.16015910005 1.02240315125 -Dl -1.13990576822 1.02617717602 -Dl -1.11853875578 1.027480546 -Dl -0.0133061004444 1.027480546 -Dl 0.008060891422 1.02617717602 -Dl 0.0283142113138 1.02240315125 -Dl 0.0471864102696 1.01636259948 -Dl 0.064410039328 1.00825964848 -Dl 0.0797176495278 0.998298426028 -Dl 0.0928417919076 0.986683059904 -Dl 0.103515017506 0.973617677884 -Dl 0.111469877362 0.959306407746 -Dl 0.116438922514 0.943953377266 -Dl 0.118154704 0.927762714222 -Dl 0.118154704 -0.9280790684 -Dl 0.116438922514 -0.944260746888 -Dl 0.111469877362 -0.959589405351 -Dl 0.103515017506 -0.973864763088 -Dl 0.0928417919076 -0.9868865394 -Dl 0.0797176495278 -0.998454453586 -Dl 0.064410039328 -1.00836822494 -Dl 0.0471864102696 -1.01642757278 -Dl 0.0283142113138 -1.02243221638 -Dl 0.008060891422 -1.02618187506 -Dl -0.0133061004444 -1.0274762681 -Dl -1.11853875578 -1.0274762681 -Dl -0.997979580533 -0.743713298222 -Dl -0.614498785111 -0.743713298222 -Dl -0.517025410222 -0.743713298222 -Dl -0.511574570222 -0.743713298222 -Dl -0.450075184458 -0.738785629162 -Dl -0.391801897737 -0.724512564011 -Dl -0.337519621914 -0.701659015808 -Dl -0.287993268846 -0.670989897596 -Dl -0.243987750389 -0.633270122417 -Dl -0.206267978398 -0.58926460331 -Dl -0.17559886473 -0.539738253318 -Dl -0.152745321241 -0.485455985483 -Dl -0.138472259786 -0.427182712844 -Dl -0.133544592222 -0.365683348444 -Dl -0.133544592222 0.365366977333 -Dl -0.138472259786 0.42687531338 -Dl -0.152745321241 0.485172952267 -Dl -0.17559886473 0.539491132513 -Dl -0.206267978398 0.58906109264 -Dl -0.243987750389 0.633114071167 -Dl -0.287993268846 0.670881306613 -Dl -0.337519621914 0.7015940375 -Dl -0.391801897737 0.724483502347 -Dl -0.450075184458 0.738780939673 -Dl -0.511574570222 0.743717588 -Dl -0.517025410222 0.743717588 -Dl -0.614498785111 0.743717588 -Dl -0.997979580533 0.743717588 -Dl -0.997979580533 -0.743713298222 -Dl -1.11853875578 -1.0274762681 -DP 0 0 0 0 70 0.00254 15 -Dl 0.391977936444 -0.747777337733 -Dl 0.382259953651 -0.746732845899 -Dl 0.373038393029 -0.74370875169 -Dl 0.364437270418 -0.738869127051 -Dl 0.356580601657 -0.732378043925 -Dl 0.349592402583 -0.724399574256 -Dl 0.343596689036 -0.715097789986 -Dl 0.338717476855 -0.70463676306 -Dl 0.335078781877 -0.693180565421 -Dl 0.332804619942 -0.680893269013 -Dl 0.332019006889 -0.667938945778 -Dl 0.332019006889 -0.624653084222 -Dl 0.332804618418 -0.611698760513 -Dl 0.335078776459 -0.599411462818 -Dl 0.338717466187 -0.587955263282 -Dl 0.34359667278 -0.577494234053 -Dl 0.349592381417 -0.568192447278 -Dl 0.356580577273 -0.560213975102 -Dl 0.364437245526 -0.553722889673 -Dl 0.373038371355 -0.548883263138 -Dl 0.382259939935 -0.545859167642 -Dl 0.391977936444 -0.544814675333 -Dl 0.693055256 -0.544814675333 -Dl 0.693055256 0.671999333333 -Dl 0.694042585976 0.684300999329 -Dl 0.69690207426 0.69596052674 -Dl 0.701479968386 0.706824163099 -Dl 0.70762251589 0.716738155943 -Dl 0.715175964306 0.725548752806 -Dl 0.723986561168 0.733102201221 -Dl 0.733900554012 0.739244748725 -Dl 0.744764190372 0.743822642852 -Dl 0.756423717782 0.746682131135 -Dl 0.768725383778 0.747669461111 -Dl 0.822912868889 0.747669461111 -Dl 0.835214534885 0.746682131135 -Dl 0.846874062295 0.743822642852 -Dl 0.857737698655 0.739244748725 -Dl 0.867651691499 0.733102201221 -Dl 0.876462288361 0.725548752806 -Dl 0.884015736777 0.716738155943 -Dl 0.890158284281 0.706824163099 -Dl 0.894736178407 0.69596052674 -Dl 0.897595666691 0.684300999329 -Dl 0.898582996667 0.671999333333 -Dl 0.898582996667 -0.544814675333 -Dl 1.19004122333 -0.544814675333 -Dl 1.19975921378 -0.545859167642 -Dl 1.20898078052 -0.548883263138 -Dl 1.2175819079 -0.553722889673 -Dl 1.22543858025 -0.560213975102 -Dl 1.23242678189 -0.568192447278 -Dl 1.23842249716 -0.577494234053 -Dl 1.24330171038 -0.587955263282 -Dl 1.2469404059 -0.599411462818 -Dl 1.24921456803 -0.611698760513 -Dl 1.25000018111 -0.624653084222 -Dl 1.25000018111 -0.667938945778 -Dl 1.24921456955 -0.680893269013 -Dl 1.24694041132 -0.693180565421 -Dl 1.24330172105 -0.70463676306 -Dl 1.23842251341 -0.715097789986 -Dl 1.23242680306 -0.724399574256 -Dl 1.22543860463 -0.732378043925 -Dl 1.21758193279 -0.738869127051 -Dl 1.2089808022 -0.74370875169 -Dl 1.19975922749 -0.746732845899 -Dl 1.19004122333 -0.747777337733 -Dl 0.391977936444 -0.747777337733 -Dl 0.391977936444 -0.747777337733 -$EndMODULE DT-Logo-Front -$MODULE DT-Logo-Back -Po 0 0 0 15 00000000 00000000 ~~ -Li DT-Logo-Back -T0 0 -4.48650007222 1.524 1.524 0 0.3048 N I 21 "DT-Logo-Back" -T1 0 4.48650007222 1.524 1.524 0 0.3048 N I 21 "G***" -DP 0 0 0 0 45 0.00254 22 -Dl 1.11500002778 -1.43850007222 -Dl -1.11500002778 -1.43850007222 -Dl -1.21826499778 -1.43021840222 -Dl -1.31612738778 -1.40623191222 -Dl -1.40729941778 -1.36782838222 -Dl -1.49049330778 -1.31629559222 -Dl -1.56442127778 -1.25292132222 -Dl -1.62779554778 -1.17899335222 -Dl -1.67932833778 -1.09579946222 -Dl -1.71773186778 -1.00462743222 -Dl -1.74171835778 -0.906765042222 -Dl -1.75000002778 -0.803500072222 -Dl -1.75000002778 0.803499987556 -Dl -1.74171835778 0.906764959926 -Dl -1.71773186778 1.00462735636 -Dl -1.67932833778 1.09579939584 -Dl -1.62779554778 1.17899329736 -Dl -1.56442127778 1.25292127989 -Dl -1.49049330778 1.31629556242 -Dl -1.40729941778 1.36782836393 -Dl -1.31612738778 1.40623190342 -Dl -1.21826499778 1.43021839985 -Dl -1.11500002778 1.43850007222 -Dl 1.11500002778 1.43850007222 -Dl 1.21826499778 1.43021839985 -Dl 1.31612738778 1.40623190342 -Dl 1.40729941778 1.36782836393 -Dl 1.49049330778 1.31629556242 -Dl 1.56442127778 1.25292127989 -Dl 1.62779554778 1.17899329736 -Dl 1.67932833778 1.09579939584 -Dl 1.71773186778 1.00462735636 -Dl 1.74171835778 0.906764959926 -Dl 1.75000002778 0.803499987556 -Dl 1.75000002778 -0.803500072222 -Dl 1.74171835778 -0.906765042222 -Dl 1.71773186778 -1.00462743222 -Dl 1.67932833778 -1.09579946222 -Dl 1.62779554778 -1.17899335222 -Dl 1.56442127778 -1.25292132222 -Dl 1.49049330778 -1.31629559222 -Dl 1.40729941778 -1.36782838222 -Dl 1.31612738778 -1.40623191222 -Dl 1.21826499778 -1.43021840222 -Dl 1.11500002778 -1.43850007222 -DP 0 0 0 0 45 0.00254 0 -Dl 0.799826470222 -0.551812572889 -Dl 0.799826470222 0.551656560444 -Dl 0.565762111778 0.551656560444 -Dl 0.561958169057 0.551588736518 -Dl 0.557617048171 0.551401158364 -Dl 0.552923190034 0.551117666424 -Dl 0.548061035561 0.550762101138 -Dl 0.543215025667 0.550358302944 -Dl 0.538569601266 0.549930112284 -Dl 0.534309203273 0.549501369598 -Dl 0.530618272603 0.549095915324 -Dl 0.52768125017 0.548737589904 -Dl 0.525682576889 0.548450233778 -Dl 0.492995048016 0.540568430728 -Dl 0.462283746331 0.52830719253 -Dl 0.433893631435 0.512020431606 -Dl 0.40816966293 0.49206206038 -Dl 0.385456800417 0.468785991278 -Dl 0.366100003497 0.442546136722 -Dl 0.350444231772 0.413696409136 -Dl 0.338834444843 0.382590720946 -Dl 0.331615602311 0.349582984574 -Dl 0.329132663778 0.315027112444 -Dl 0.329132663778 -0.315022850889 -Dl 0.331662301481 -0.349341511769 -Dl 0.339000486336 -0.381745179104 -Dl 0.350771125962 -0.412034607335 -Dl 0.366598127977 -0.440010550903 -Dl 0.3861054 -0.46547376425 -Dl 0.40891684965 -0.488225001817 -Dl 0.434656384545 -0.508065018045 -Dl 0.462947912304 -0.524794567376 -Dl 0.493415340546 -0.538214404251 -Dl 0.525682576889 -0.548125283111 -Dl 0.528861655226 -0.548802540244 -Dl 0.532532627931 -0.549415840178 -Dl 0.536580107545 -0.549963455711 -Dl 0.54088870661 -0.550443659644 -Dl 0.545343037667 -0.550854724778 -Dl 0.549827713257 -0.551194923911 -Dl 0.554227345922 -0.551462529844 -Dl 0.558426548203 -0.551655815378 -Dl 0.562309932641 -0.551773053311 -Dl 0.565762111778 -0.551812516444 -Dl 0.799826470222 -0.551812572889 -DP 0 0 0 0 75 0.00254 0 -Dl 1.11853875578 -1.0274762681 -Dl 1.13990576822 -1.02618187506 -Dl 1.16015910005 -1.02243221638 -Dl 1.17903130383 -1.01642757278 -Dl 1.19625493213 -1.00836822494 -Dl 1.2115625375 -0.998454453586 -Dl 1.22468667251 -0.9868865394 -Dl 1.23535988973 -0.973864763088 -Dl 1.24331474171 -0.959589405351 -Dl 1.24828378102 -0.944260746888 -Dl 1.24999956022 -0.9280790684 -Dl 1.24999956022 0.927762714222 -Dl 1.24828378102 0.943953377266 -Dl 1.24331474171 0.959306407746 -Dl 1.23535988973 0.973617677884 -Dl 1.22468667251 0.986683059904 -Dl 1.2115625375 0.998298426028 -Dl 1.19625493213 1.00825964848 -Dl 1.17903130383 1.01636259948 -Dl 1.16015910005 1.02240315125 -Dl 1.13990576822 1.02617717602 -Dl 1.11853875578 1.027480546 -Dl 0.0133061004444 1.027480546 -Dl -0.008060891422 1.02617717602 -Dl -0.0283142113138 1.02240315125 -Dl -0.0471864102696 1.01636259948 -Dl -0.064410039328 1.00825964848 -Dl -0.0797176495278 0.998298426028 -Dl -0.0928417919076 0.986683059904 -Dl -0.103515017506 0.973617677884 -Dl -0.111469877362 0.959306407746 -Dl -0.116438922514 0.943953377266 -Dl -0.118154704 0.927762714222 -Dl -0.118154704 -0.9280790684 -Dl -0.116438922514 -0.944260746888 -Dl -0.111469877362 -0.959589405351 -Dl -0.103515017506 -0.973864763088 -Dl -0.0928417919076 -0.9868865394 -Dl -0.0797176495278 -0.998454453586 -Dl -0.064410039328 -1.00836822494 -Dl -0.0471864102696 -1.01642757278 -Dl -0.0283142113138 -1.02243221638 -Dl -0.008060891422 -1.02618187506 -Dl 0.0133061004444 -1.0274762681 -Dl 1.11853875578 -1.0274762681 -Dl 0.997979580533 -0.743713298222 -Dl 0.614498785111 -0.743713298222 -Dl 0.517025410222 -0.743713298222 -Dl 0.511574570222 -0.743713298222 -Dl 0.450075184458 -0.738785629162 -Dl 0.391801897737 -0.724512564011 -Dl 0.337519621914 -0.701659015808 -Dl 0.287993268846 -0.670989897596 -Dl 0.243987750389 -0.633270122417 -Dl 0.206267978398 -0.58926460331 -Dl 0.17559886473 -0.539738253318 -Dl 0.152745321241 -0.485455985483 -Dl 0.138472259786 -0.427182712844 -Dl 0.133544592222 -0.365683348444 -Dl 0.133544592222 0.365366977333 -Dl 0.138472259786 0.42687531338 -Dl 0.152745321241 0.485172952267 -Dl 0.17559886473 0.539491132513 -Dl 0.206267978398 0.58906109264 -Dl 0.243987750389 0.633114071167 -Dl 0.287993268846 0.670881306613 -Dl 0.337519621914 0.7015940375 -Dl 0.391801897737 0.724483502347 -Dl 0.450075184458 0.738780939673 -Dl 0.511574570222 0.743717588 -Dl 0.517025410222 0.743717588 -Dl 0.614498785111 0.743717588 -Dl 0.997979580533 0.743717588 -Dl 0.997979580533 -0.743713298222 -Dl 1.11853875578 -1.0274762681 -DP 0 0 0 0 70 0.00254 0 -Dl -0.391977936444 -0.747777337733 -Dl -0.382259953651 -0.746732845899 -Dl -0.373038393029 -0.74370875169 -Dl -0.364437270418 -0.738869127051 -Dl -0.356580601657 -0.732378043925 -Dl -0.349592402583 -0.724399574256 -Dl -0.343596689036 -0.715097789986 -Dl -0.338717476855 -0.70463676306 -Dl -0.335078781877 -0.693180565421 -Dl -0.332804619942 -0.680893269013 -Dl -0.332019006889 -0.667938945778 -Dl -0.332019006889 -0.624653084222 -Dl -0.332804618418 -0.611698760513 -Dl -0.335078776459 -0.599411462818 -Dl -0.338717466187 -0.587955263282 -Dl -0.34359667278 -0.577494234053 -Dl -0.349592381417 -0.568192447278 -Dl -0.356580577273 -0.560213975102 -Dl -0.364437245526 -0.553722889673 -Dl -0.373038371355 -0.548883263138 -Dl -0.382259939935 -0.545859167642 -Dl -0.391977936444 -0.544814675333 -Dl -0.693055256 -0.544814675333 -Dl -0.693055256 0.671999333333 -Dl -0.694042585976 0.684300999329 -Dl -0.69690207426 0.69596052674 -Dl -0.701479968386 0.706824163099 -Dl -0.70762251589 0.716738155943 -Dl -0.715175964306 0.725548752806 -Dl -0.723986561168 0.733102201221 -Dl -0.733900554012 0.739244748725 -Dl -0.744764190372 0.743822642852 -Dl -0.756423717782 0.746682131135 -Dl -0.768725383778 0.747669461111 -Dl -0.822912868889 0.747669461111 -Dl -0.835214534885 0.746682131135 -Dl -0.846874062295 0.743822642852 -Dl -0.857737698655 0.739244748725 -Dl -0.867651691499 0.733102201221 -Dl -0.876462288361 0.725548752806 -Dl -0.884015736777 0.716738155943 -Dl -0.890158284281 0.706824163099 -Dl -0.894736178407 0.69596052674 -Dl -0.897595666691 0.684300999329 -Dl -0.898582996667 0.671999333333 -Dl -0.898582996667 -0.544814675333 -Dl -1.19004122333 -0.544814675333 -Dl -1.19975921378 -0.545859167642 -Dl -1.20898078052 -0.548883263138 -Dl -1.2175819079 -0.553722889673 -Dl -1.22543858025 -0.560213975102 -Dl -1.23242678189 -0.568192447278 -Dl -1.23842249716 -0.577494234053 -Dl -1.24330171038 -0.587955263282 -Dl -1.2469404059 -0.599411462818 -Dl -1.24921456803 -0.611698760513 -Dl -1.25000018111 -0.624653084222 -Dl -1.25000018111 -0.667938945778 -Dl -1.24921456955 -0.680893269013 -Dl -1.24694041132 -0.693180565421 -Dl -1.24330172105 -0.70463676306 -Dl -1.23842251341 -0.715097789986 -Dl -1.23242680306 -0.724399574256 -Dl -1.22543860463 -0.732378043925 -Dl -1.21758193279 -0.738869127051 -Dl -1.2089808022 -0.74370875169 -Dl -1.19975922749 -0.746732845899 -Dl -1.19004122333 -0.747777337733 -Dl -0.391977936444 -0.747777337733 -Dl -0.391977936444 -0.747777337733 -$EndMODULE DT-Logo-Back -$EndLIBRARY \ No newline at end of file diff --git a/examples/dt-logo.svg b/examples/dt-logo.svg deleted file mode 100644 index 9987a91..0000000 --- a/examples/dt-logo.svg +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/svg2mod.svg b/examples/svg2mod.svg new file mode 100644 index 0000000..6a123e2 --- /dev/null +++ b/examples/svg2mod.svg @@ -0,0 +1,160 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 415fcf68f786e8d1946da4ba13f79762df90baab Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Fri, 9 Oct 2020 13:53:23 -0600 Subject: [PATCH 040/151] Update version and fix .travis --- .travis.yml | 2 +- setup.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ed3b24d..21e3178 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,2 +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 +script: pip3 install ./ && svg2mod -i examples/svg2mod.svg -o output.kicad_mod --name TEST --value VALUE -f 2 -p 1 --format pretty --units mm -d 300 --center -x && cat output.kicad_mod diff --git a/setup.py b/setup.py index a05f4e4..4196a3c 100644 --- a/setup.py +++ b/setup.py @@ -22,12 +22,12 @@ setup( name='svg2mod', - version='0.1.0', + version='0.1.1', description="Convert an SVG file to a KiCad footprint.", long_description=readme, - author='https://github.com/Sodium-Hydrogen', + author='https://github.com/svg2mod', author_email='', - url='https://github.com/Sodium-Hydrogen/svg2mod', + url='https://github.com/svg2mod/svg2mod', packages=setuptools.find_packages(), entry_points={'console_scripts':['svg2mod = svg2mod.svg2mod:main']}, package_dir={'svg2mod':'svg2mod'}, From 6e648c4897ae7cc2f4f7920cdaa4a16a549be5d3 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Fri, 9 Oct 2020 14:06:23 -0600 Subject: [PATCH 041/151] Fix readme instructions --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cfbc7a4..b326912 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ If building fails make sure setuptools is up to date. `pip3 install setuptools - ``` usage: svg2mod [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] [-f FACTOR] [-p PRECISION] [--format FORMAT] [--units UNITS] - [-d DPI] [--center] + [-d DPI] [--center] [-x] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -46,6 +46,7 @@ optional arguments: --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 + -x Do not export hidden layers ``` ## SVG Files From eb527e6664034b870792ce63dd9b6b2b887bf3f8 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Fri, 9 Oct 2020 18:04:36 -0600 Subject: [PATCH 042/151] Update documentation because package is on pypi --- README.md | 3 +-- setup.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b326912..6ea2946 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,8 @@ Python 3 ## Installation -```pip3 install git+https://github.com/svg2mod/svg2mod``` +```pip3 install svg2mod``` -If building fails make sure setuptools is up to date. `pip3 install setuptools --upgrade` ## Example diff --git a/setup.py b/setup.py index 4196a3c..15a53d1 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ name='svg2mod', version='0.1.1', description="Convert an SVG file to a KiCad footprint.", + long_description_content_type='text/markdown', long_description=readme, author='https://github.com/svg2mod', author_email='', @@ -41,7 +42,7 @@ classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', + 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', 'Natural Language :: English', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.5', From 05a476ae575dbdd66a8956812376b9acc6b567d5 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 13 Oct 2020 12:55:00 -0600 Subject: [PATCH 043/151] Updated travis to build/publish, Added pad creation --- .travis.yml | 23 +++++++- README.md | 76 ++++++++++++------------ setup.py | 10 +++- svg2mod/svg2mod.py | 144 ++++++++++++++++++++++++++++++--------------- 4 files changed, 167 insertions(+), 86 deletions(-) diff --git a/.travis.yml b/.travis.yml index 21e3178..90755c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,2 +1,21 @@ -language: python -script: pip3 install ./ && svg2mod -i examples/svg2mod.svg -o output.kicad_mod --name TEST --value VALUE -f 2 -p 1 --format pretty --units mm -d 300 --center -x && cat output.kicad_mod +jobs: + include: + - stage: unit tests + os: linux + language: python + script: + - pip3 install ./ + - svg2mod -i examples/svg2mod.svg -o output.kicad_mod --name TEST --value VALUE -f 2 -p 1 --format pretty --units mm -d 300 --center -x -p + - svg2mod -i examples/svg2mod.svg + - stage: deploy + os: linux + language: python + script: + - python setup.py bdist_wheel sdist + deploy: + provider: pypi + username: "__token__" + password: + secure: "iPZUYg6L0em9rPO8oFzRfxLRYR+mzfyKMuOFLO5oO+zHjwZvE0XHbWc8Mcw5tzv9rEHFIKaZ66z8hiXEdEjgG6QQhqASf3A9E0ZmLcED7vedV479oC9xxK7q8ohG4vqgAuw6c9G35UZIQRah5hvXBe10OdPCGxKFG+Tw2vkT3XMZLGBevEPJ6V491+BoqGE2BvD5dwMvzz62dC/0EQffN2jT6daDDHj3tYSQdydD1HYzUl9LQhNtIXdWRdORhwznn8EYQHfY/DwAhktSQGqqS+94t5oqXo3fXlwkJTZtWwr6d83gsn3zrU4FvmGACUsHjwCf5DHBx8EN9fR8eEdS6mJHXSdYNRtnV53NvY144dxUQlNs0UopZtLTofsVw+dlYSTCk/haBmGLMNfwn/dHtQz73ExpLyX3aTHpNwIzPI+Ty3xX/VvAdsHxeGEazW0/A8cDfOTM9PRDzt9eawGhGFsrP9qtiOn340WTL2si+AJtVLB9kq/eBG3ruUhZ5+XSiax20ZCb8gjEWJMdQXiFZ61tKvzfUBDXcJprBMtjcEJC4hhkfyxMS2r2haCovG3F0xx/CCfT5t0Q99wo6wukkfPpxsuENYlB2oxFr13naCV2HA60bYBGpR1fAIeaKFNMK86JNov562+dCAAzGi2MwYBQG2EJUaN8619RjwaFP+0=" + server: https://test.pypi.org + cleanup: false diff --git a/README.md b/README.md index 6ea2946..ce69629 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ __[@mtl](https://github.com/mtl) is no longer active. [https://github.com/svg2mod/svg2mod](https://github.com/svg2mod/svg2mod) is now the maintained branch.__ -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. +This is a small program to convert Inkscape SVG drawings to KiCad footprint module files. It uses [a fork of cjlano's python SVG parser and drawing module](https://github.com/svg2mod/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 @@ -20,9 +20,9 @@ Python 3 ## Usage ``` -usage: svg2mod [-h] -i FILENAME [-o FILENAME] [--name NAME] [--value VALUE] - [-f FACTOR] [-p PRECISION] [--format FORMAT] [--units UNITS] - [-d DPI] [--center] [-x] +usage: svg2mod [-h] -i FILENAME [-o FILENAME] [-c] [-pads] [-x] [-d DPI] + [-f FACTOR] [-p PRECISION] [--format FORMAT] [--name NAME] + [--units UNITS] [--value VALUE] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -32,20 +32,22 @@ optional arguments: name of the SVG file -o FILENAME, --output-file FILENAME name of the module file - --name NAME, --module-name NAME - base name of the module - --value VALUE, --module-value VALUE - value of the module + -c, --center Center the module to the center of the bounding box + -pads, --convert-pads + Convert any artwork on Cu layers to pads + -x, --exclude-hidden Do not export hidden layers + -d DPI, --dpi DPI DPI of the SVG file (int) -f FACTOR, --factor FACTOR scale paths by this factor -p PRECISION, --precision PRECISION smoothness for approximating curves with line segments (float) --format FORMAT output module file format (legacy|pretty) + --name NAME, --module-name NAME + base name of the module --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 - -x Do not export hidden layers + --value VALUE, --module-value VALUE + value of the module ``` ## SVG Files @@ -54,36 +56,36 @@ svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain * 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. * Paths are supported. * A path may have an outline and a fill. (Colors will be ignored.) - * 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. + * A path may have holes, defined by interior segments within the path (see included examples). + * A path with a filled area inside a hole will not work properly. You must split these apart into two seperate paths. * 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 named according to the rules below. + * Layers must be named to match the target in kicad. The supported layers are listed 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. ### Layers -Layers must be named (case-insensitive) according to the following rules: - -| Inkscape layer name | KiCad layer(s) | KiCad legacy | KiCad pretty | -|:-------------------:|:----------------:|:------------:|:------------:| -| 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 | -| F.Fab | F.Fab | -- | Yes | -| B.Fab | B.Fab | -- | Yes | -| F.CrtYd | F.CrtYd | -- | Yes | -| B.CrtYd | B.CrtYd | -- | Yes | +This supports the layers listed below. They are the same in inkscape and kicad: + +| KiCad layer(s) | KiCad legacy | KiCad pretty | +|:----------------:|:------------:|:------------:| +| F.Cu | Yes | Yes | +| B.Cu | Yes | Yes | +| F.Adhes | Yes | Yes | +| B.Adhes | Yes | Yes | +| F.Paste | Yes | Yes | +| B.Paste | Yes | Yes | +| F.SilkS | Yes | Yes | +| B.SilkS | Yes | Yes | +| F.Mask | Yes | Yes | +| B.Mask | Yes | Yes | +| Dwgs.User | Yes | Yes | +| Cmts.User | Yes | Yes | +| Eco1.User | Yes | Yes | +| Eco2.User | Yes | Yes | +| Edge.Cuts | Yes | Yes | +| F.Fab | -- | Yes | +| B.Fab | -- | Yes | +| F.CrtYd | -- | Yes | +| B.CrtYd | -- | Yes | 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 15a53d1..3f6bd86 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import os import setuptools @@ -13,6 +14,13 @@ with open('README.md') as readme_file: readme = readme_file.read() +tag = "" + +try: + tag = os.popen("git describe --tag")._stream.read().strip() +except: + tag = "develpment" + requirements = [ ] @@ -22,7 +30,7 @@ setup( name='svg2mod', - version='0.1.1', + version=tag, description="Convert an SVG file to a KiCad footprint.", long_description_content_type='text/markdown', long_description=readme, diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 63ee64d..c03df4b 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -62,7 +62,8 @@ def main(): args.center, args.scale_factor, args.precision, - args.dpi, + dpi = args.dpi, + pads = args.convert_to_pads ) else: @@ -540,6 +541,7 @@ def __init__( precision = 20.0, use_mm = True, dpi = DEFAULT_DPI, + pads = False, ): if use_mm: # 25.4 mm/in; @@ -556,6 +558,7 @@ def __init__( self.precision = precision self.use_mm = use_mm self.dpi = dpi + self.convert_pads = pads #------------------------------------------------------------------------ @@ -1321,6 +1324,17 @@ def _write_modules( self ): self._write_module( front = True ) + #------------------------------------------------------------------------ + + def _write_polygon_filled( self, points, layer, stroke_width = 0): + self._write_polygon_header( points, layer, stroke_width) + + for point in points: + self._write_polygon_point( point ) + + self._write_polygon_footer( layer, stroke_width ) + + #------------------------------------------------------------------------ def _write_polygon( self, points, layer, fill, stroke, stroke_width ): @@ -1343,17 +1357,44 @@ def _write_polygon( self, points, layer, fill, stroke, stroke_width ): def _write_polygon_footer( self, layer, stroke_width ): - self.output_file.write( - " )\n (layer {})\n (width {})\n )".format( - layer, stroke_width + if self._create_pad: + self.output_file.write( + " )\n (width {}) )\n ))".format( + stroke_width + ) + ) + else: + self.output_file.write( + " )\n (layer {})\n (width {})\n )".format( + layer, stroke_width + ) ) - ) #------------------------------------------------------------------------ - def _write_polygon_header( self, points, layer ): + def _write_polygon_header( self, points, layer, stroke_width): + self._create_pad = self.convert_pads and layer.find("Cu") == 2 + if stroke_width == 0: + stroke_width = 1e-5 #This is the smallest a pad can be and still be rendered in kicad + if self._create_pad: + self.output_file.write( '''\n (pad 1 smd custom (at {0} {1}) (size {2:.6f} {2:.6f}) (layers {3}) + (zone_connect 0) + (options (clearance outline) (anchor circle)) + (primitives\n (gr_poly (pts \n'''.format( + points[0].x, #0 + points[0].y, #1 + stroke_width, #2 + layer, #3 + ) + ) + originx = points[0].x + originy = points[0].y + for point in points: + point.x = point.x-originx + point.y = point.y-originy + else: self.output_file.write( "\n (fp_poly\n (pts \n" ) @@ -1361,9 +1402,12 @@ def _write_polygon_header( self, points, layer ): def _write_polygon_point( self, point ): - self.output_file.write( - " (xy {} {})\n".format( point.x, point.y ) - ) + if self._create_pad: + self.output_file.write(" ") + + self.output_file.write( + " (xy {} {})\n".format( point.x, point.y ) + ) #------------------------------------------------------------------------ @@ -1417,21 +1461,39 @@ def get_arguments(): ) parser.add_argument( - '--name', '--module-name', - type = str, - dest = 'module_name', - metavar = 'NAME', - help = "base name of the module", - default = "svg2mod", + '-c', '--center', + dest = 'center', + action = 'store_const', + const = True, + help = "Center the module to the center of the bounding box", + default = False, ) parser.add_argument( - '--value', '--module-value', - type = str, - dest = 'module_value', - metavar = 'VALUE', - help = "value of the module", - default = "G***", + '-pads', '--convert-pads', + dest = 'convert_to_pads', + action = 'store_const', + const = True, + help = "Convert any artwork on Cu layers to pads", + default = False, + ) + + parser.add_argument( + '-x', '--exclude-hidden', + dest = 'ignore_hidden_layers', + action = 'store_const', + const = True, + help = "Do not export hidden layers", + default = False, + ) + + parser.add_argument( + '-d', '--dpi', + type = int, + dest = 'dpi', + metavar = 'DPI', + help = "DPI of the SVG file (int)", + default = DEFAULT_DPI, ) parser.add_argument( @@ -1451,7 +1513,6 @@ def get_arguments(): help = "smoothness for approximating curves with line segments (float)", default = 10.0, ) - parser.add_argument( '--format', type = str, @@ -1462,6 +1523,15 @@ def get_arguments(): default = 'pretty', ) + parser.add_argument( + '--name', '--module-name', + type = str, + dest = 'module_name', + metavar = 'NAME', + help = "base name of the module", + default = "svg2mod", + ) + parser.add_argument( '--units', type = str, @@ -1473,30 +1543,12 @@ def get_arguments(): ) parser.add_argument( - '-d', '--dpi', - type = int, - dest = 'dpi', - metavar = 'DPI', - help = "DPI of the SVG file (int)", - 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, - ) - - parser.add_argument( - '-x', - dest = 'ignore_hidden_layers', - action = 'store_const', - const = True, - help = "Do not export hidden layers", - default = False, + '--value', '--module-value', + type = str, + dest = 'module_value', + metavar = 'VALUE', + help = "value of the module", + default = "G***", ) return parser.parse_args(), parser From 1bb542c78bf56a4a6b11ed0618f2898ccc811e5d Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 13 Oct 2020 12:59:36 -0600 Subject: [PATCH 044/151] Fix error with .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 90755c4..d52e2d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ jobs: language: python script: - pip3 install ./ - - svg2mod -i examples/svg2mod.svg -o output.kicad_mod --name TEST --value VALUE -f 2 -p 1 --format pretty --units mm -d 300 --center -x -p + - svg2mod -i examples/svg2mod.svg -o output.kicad_mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format pretty --units mm -d 300 - svg2mod -i examples/svg2mod.svg - stage: deploy os: linux From 348074d9c7c61697d3cae4abe7175e2b94f638d6 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 13 Oct 2020 13:08:55 -0600 Subject: [PATCH 045/151] More deploy tweaks --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d52e2d6..f9c4e59 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,5 +17,7 @@ jobs: username: "__token__" password: secure: "iPZUYg6L0em9rPO8oFzRfxLRYR+mzfyKMuOFLO5oO+zHjwZvE0XHbWc8Mcw5tzv9rEHFIKaZ66z8hiXEdEjgG6QQhqASf3A9E0ZmLcED7vedV479oC9xxK7q8ohG4vqgAuw6c9G35UZIQRah5hvXBe10OdPCGxKFG+Tw2vkT3XMZLGBevEPJ6V491+BoqGE2BvD5dwMvzz62dC/0EQffN2jT6daDDHj3tYSQdydD1HYzUl9LQhNtIXdWRdORhwznn8EYQHfY/DwAhktSQGqqS+94t5oqXo3fXlwkJTZtWwr6d83gsn3zrU4FvmGACUsHjwCf5DHBx8EN9fR8eEdS6mJHXSdYNRtnV53NvY144dxUQlNs0UopZtLTofsVw+dlYSTCk/haBmGLMNfwn/dHtQz73ExpLyX3aTHpNwIzPI+Ty3xX/VvAdsHxeGEazW0/A8cDfOTM9PRDzt9eawGhGFsrP9qtiOn340WTL2si+AJtVLB9kq/eBG3ruUhZ5+XSiax20ZCb8gjEWJMdQXiFZ61tKvzfUBDXcJprBMtjcEJC4hhkfyxMS2r2haCovG3F0xx/CCfT5t0Q99wo6wukkfPpxsuENYlB2oxFr13naCV2HA60bYBGpR1fAIeaKFNMK86JNov562+dCAAzGi2MwYBQG2EJUaN8619RjwaFP+0=" - server: https://test.pypi.org + server: "https://test.pypi.org/legacy/" cleanup: false + on: + tags: true From 205c1f8e2babbdc4b57de410f97504c1ee796ca7 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 13 Oct 2020 13:21:00 -0600 Subject: [PATCH 046/151] test publishing --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f9c4e59..5af5c57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ jobs: provider: pypi username: "__token__" password: - secure: "iPZUYg6L0em9rPO8oFzRfxLRYR+mzfyKMuOFLO5oO+zHjwZvE0XHbWc8Mcw5tzv9rEHFIKaZ66z8hiXEdEjgG6QQhqASf3A9E0ZmLcED7vedV479oC9xxK7q8ohG4vqgAuw6c9G35UZIQRah5hvXBe10OdPCGxKFG+Tw2vkT3XMZLGBevEPJ6V491+BoqGE2BvD5dwMvzz62dC/0EQffN2jT6daDDHj3tYSQdydD1HYzUl9LQhNtIXdWRdORhwznn8EYQHfY/DwAhktSQGqqS+94t5oqXo3fXlwkJTZtWwr6d83gsn3zrU4FvmGACUsHjwCf5DHBx8EN9fR8eEdS6mJHXSdYNRtnV53NvY144dxUQlNs0UopZtLTofsVw+dlYSTCk/haBmGLMNfwn/dHtQz73ExpLyX3aTHpNwIzPI+Ty3xX/VvAdsHxeGEazW0/A8cDfOTM9PRDzt9eawGhGFsrP9qtiOn340WTL2si+AJtVLB9kq/eBG3ruUhZ5+XSiax20ZCb8gjEWJMdQXiFZ61tKvzfUBDXcJprBMtjcEJC4hhkfyxMS2r2haCovG3F0xx/CCfT5t0Q99wo6wukkfPpxsuENYlB2oxFr13naCV2HA60bYBGpR1fAIeaKFNMK86JNov562+dCAAzGi2MwYBQG2EJUaN8619RjwaFP+0=" + secure: "VkDOh68XBeP6bZrCnBmWoKz2eRZeKek/29M2tzC3I42wci2KFWCRJEiXfIAq1qwl4lw1RUVtSc6XrJNskgxKVrZgGNZTp7slLZNOBRMA/A/NHwF65OuBxwPta9o0l5kUWYc72AkXUD/uaWN9NLrJrMw/HSoFPdl8reWzJq4zUpJPP0/dDvcNe4oFbqUvDdV9ZaVbd9m9kvaY/KvjCiJ36tTRHFac66y5fLz3heKVPwXQT+qYG2NCM1o8Iik16+zCbxIrTtHCpb8sYXTmHBuVHacmy2xG1TPIvkIG+toSfK+cQHBLjyZXIcgfmIlusxQ+Acdv2QSDOT1Miyhdz+MRFYC7UQIDQn39mE4gLOy9ljYoU/v4TwzZ5xjC0cfp9D3N2Bt7STkM9u/Ft6jEXsVfvJ0MbHu4UDk/xK5AngDk8g3F46sje01yfc8D43Ghgb4Ixd9i76VkXm6RRSKZwCtR1DYYYTS93yAKdALj7qdEMzbANURGI1ly9bkWWRcz30LB1Ufon6gO5LdU0mzjGEj2Yl2n22RKCtLG0mNo+I9xrXcicfpCn4mbQ25o1dvbG2ROYFWXYa/yaK/K/PjtAHa2sFEiuqSDlUFd/qfjB+37u/QPuxZSf99P5KBQ4Pc0pvCjOlGqHWI6jzhPp/S6DnyXGykuPn8/IATe9FrUFkEjDlo=" server: "https://test.pypi.org/legacy/" cleanup: false on: From b60662a393d8d50b0b4a9ad5fda6708fab276b3f Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 13 Oct 2020 13:35:27 -0600 Subject: [PATCH 047/151] Finalize travis for official deploy --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5af5c57..04d1fd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,8 @@ jobs: provider: pypi username: "__token__" password: - secure: "VkDOh68XBeP6bZrCnBmWoKz2eRZeKek/29M2tzC3I42wci2KFWCRJEiXfIAq1qwl4lw1RUVtSc6XrJNskgxKVrZgGNZTp7slLZNOBRMA/A/NHwF65OuBxwPta9o0l5kUWYc72AkXUD/uaWN9NLrJrMw/HSoFPdl8reWzJq4zUpJPP0/dDvcNe4oFbqUvDdV9ZaVbd9m9kvaY/KvjCiJ36tTRHFac66y5fLz3heKVPwXQT+qYG2NCM1o8Iik16+zCbxIrTtHCpb8sYXTmHBuVHacmy2xG1TPIvkIG+toSfK+cQHBLjyZXIcgfmIlusxQ+Acdv2QSDOT1Miyhdz+MRFYC7UQIDQn39mE4gLOy9ljYoU/v4TwzZ5xjC0cfp9D3N2Bt7STkM9u/Ft6jEXsVfvJ0MbHu4UDk/xK5AngDk8g3F46sje01yfc8D43Ghgb4Ixd9i76VkXm6RRSKZwCtR1DYYYTS93yAKdALj7qdEMzbANURGI1ly9bkWWRcz30LB1Ufon6gO5LdU0mzjGEj2Yl2n22RKCtLG0mNo+I9xrXcicfpCn4mbQ25o1dvbG2ROYFWXYa/yaK/K/PjtAHa2sFEiuqSDlUFd/qfjB+37u/QPuxZSf99P5KBQ4Pc0pvCjOlGqHWI6jzhPp/S6DnyXGykuPn8/IATe9FrUFkEjDlo=" - server: "https://test.pypi.org/legacy/" + secure: "xK2WHvqIcBrNxprKkA69VcdkLBZg2/sn9Dfus88VLxNWket3fwshIXzzttQndL+4cjZTkJvBWuMToYvvYCV0ZIfgOuVbY4ZeWvTAKB4CpN/gPgpcXfbbJXbN0AVtzICOaLBgdcROz5MxwJ95Dx5LG+Z2O8jYIWzHtdCgFwXlE2mbyN9/+KCKVTR7qBTtJVGr94YwuFuK11WN6tn1s681zYZFE0pJLdI+pgxW7xWZFSh680dV8clfMp4yoon47q2tmP3KI8sXbZXXYcfBu/HDB0lLrRajAAm6uIAlRNilPDzzLagJh7TL+1hzIC/J5wdMtO8NMovwcKNFjSJkPfiuv8ljphaQGqYsTiA6cK3ZMOOvpmtFlsJE+aiCDmulVVhdlydaks89Ra4MV+rYrn1z5SV6SYMDWIGiOxqfYS7D8R4RGkCx9JIb1QJO984Cju422eoa22f7ELWEcfu2IMh50m7Pv9EdKzE0Pd8XCIQl/fc21pzVa0gZiKfOAcw8l1N+9tqIw8NvFjnzg3WxfyAOZT2SPrVP1cTYGT75yPhAizCCc+aqgD3h+PnjzmOHR5zRAZnLqQW0Q9CFxXHSQnOXfYbMbHp9kWbGLT+2BOyz4mMAE/0guI7lD4OZ26QAV8sYpVrlqox44BazG5sPc1EvtkA/bCJQsl3OXcY3rfwMCRU=" cleanup: false on: + branch: master tags: true From 3505b9777fe662afff73d25c03745dbe8b2efa04 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 13 Oct 2020 13:56:06 -0600 Subject: [PATCH 048/151] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 04d1fd7..d01a76f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ jobs: provider: pypi username: "__token__" password: - secure: "xK2WHvqIcBrNxprKkA69VcdkLBZg2/sn9Dfus88VLxNWket3fwshIXzzttQndL+4cjZTkJvBWuMToYvvYCV0ZIfgOuVbY4ZeWvTAKB4CpN/gPgpcXfbbJXbN0AVtzICOaLBgdcROz5MxwJ95Dx5LG+Z2O8jYIWzHtdCgFwXlE2mbyN9/+KCKVTR7qBTtJVGr94YwuFuK11WN6tn1s681zYZFE0pJLdI+pgxW7xWZFSh680dV8clfMp4yoon47q2tmP3KI8sXbZXXYcfBu/HDB0lLrRajAAm6uIAlRNilPDzzLagJh7TL+1hzIC/J5wdMtO8NMovwcKNFjSJkPfiuv8ljphaQGqYsTiA6cK3ZMOOvpmtFlsJE+aiCDmulVVhdlydaks89Ra4MV+rYrn1z5SV6SYMDWIGiOxqfYS7D8R4RGkCx9JIb1QJO984Cju422eoa22f7ELWEcfu2IMh50m7Pv9EdKzE0Pd8XCIQl/fc21pzVa0gZiKfOAcw8l1N+9tqIw8NvFjnzg3WxfyAOZT2SPrVP1cTYGT75yPhAizCCc+aqgD3h+PnjzmOHR5zRAZnLqQW0Q9CFxXHSQnOXfYbMbHp9kWbGLT+2BOyz4mMAE/0guI7lD4OZ26QAV8sYpVrlqox44BazG5sPc1EvtkA/bCJQsl3OXcY3rfwMCRU=" + secure: "gcUUJQGNThihHXodH3AbLkr+mb7mNeB9z1bLaeAPqNU+xgP5mWeh8Cy/86T98GfOxLcfyxLi8bzDlZOBUqpQRGC5xghp8vEKJoecam1rLFnZyQk7MzttO3qIqSNiO7xsnsvkIKlRtxRi/8bn+I7lnHaISCLwfkyc6/3izlKX7nGSDARKWG6zYSLgD9X1pul1gOFVE7gcEle7ndU5sL89Vj6bZB8jnexsrG4ulKlzJPE0rDJpu94ByYOCJ9CjC4QAzcQ7bhomOEugRgWYhpeSaphaRQpBJgUSFkQ2Rc5tuygKXlWJrhuWn3URGFW15ZT1kfYZH8PNR+6ozeAKrjRoFGME040yEwaYvA1ms18G6VvCX41Eq4Veou3CGvIx9cGADhTxPO0Cc5gtgx6a3rHgLB1F62u+Bhcb5ALbdQl398Phri/mXTpAthsgpSn65GKnekmre8jUKFj9rnBkdBslLnjV1S0dJ2t1/hB6dirv/lsrv1QwFt0ig8NrFs6z2N+hI1A851XW9Gja32iCRaU/AL4cnpL4dVmF8LUQRMKaoncBrCjB0nW6SiU6Hw4SDdmhSVnDC0dW8pceslZ14vZaoUQkIkg1g5TcYQ/WWweBQpnwT97GpYBrrAlGpp36DY+LGj8e3VVQnhv6ORo5aiPfrSF4Kufn+uUIaaMetph7Xgk=" cleanup: false on: branch: master From 3739b5a11d087532e377d5041f5e7cf2a366fbd2 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 13 Oct 2020 15:42:57 -0600 Subject: [PATCH 049/151] Update Doc and remove support for python 2.7 --- README.md | 10 +++++++++- setup.py | 3 +-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ce69629..cb5ab85 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # svg2mod -[![Build Status](https://travis-ci.org/svg2mod/svg2mod.svg?branch=master)](https://travis-ci.org/svg2mod/svg2mod) +[![Build Status](https://travis-ci.com/svg2mod/svg2mod.svg?branch=master)](https://travis-ci.com/svg2mod/svg2mod) [![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod)](https://github.com/svg2mod/svg2mod/commits/master) + +[![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=black)](https://pypi.org/project/svg2mod/) + +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod)](https://pypi.org/project/svg2mod/) + +[![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version)](https://pypi.org/project/svg2mod/) + __[@mtl](https://github.com/mtl) is no longer active. [https://github.com/svg2mod/svg2mod](https://github.com/svg2mod/svg2mod) is now the maintained branch.__ @@ -89,3 +96,4 @@ This supports the layers listed below. They are the same in inkscape and kicad: | B.CrtYd | -- | Yes | 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 3f6bd86..9e6b6f3 100644 --- a/setup.py +++ b/setup.py @@ -52,8 +52,7 @@ 'Intended Audience :: Science/Research', 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', 'Natural Language :: English', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.7', ], test_suite='tests', tests_require=test_requirements From dfbabc6fe863b039e6ca10ca6d31dc0f6325492b Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Tue, 13 Oct 2020 16:34:22 -0600 Subject: [PATCH 050/151] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d01a76f..2780edf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ jobs: provider: pypi username: "__token__" password: - secure: "gcUUJQGNThihHXodH3AbLkr+mb7mNeB9z1bLaeAPqNU+xgP5mWeh8Cy/86T98GfOxLcfyxLi8bzDlZOBUqpQRGC5xghp8vEKJoecam1rLFnZyQk7MzttO3qIqSNiO7xsnsvkIKlRtxRi/8bn+I7lnHaISCLwfkyc6/3izlKX7nGSDARKWG6zYSLgD9X1pul1gOFVE7gcEle7ndU5sL89Vj6bZB8jnexsrG4ulKlzJPE0rDJpu94ByYOCJ9CjC4QAzcQ7bhomOEugRgWYhpeSaphaRQpBJgUSFkQ2Rc5tuygKXlWJrhuWn3URGFW15ZT1kfYZH8PNR+6ozeAKrjRoFGME040yEwaYvA1ms18G6VvCX41Eq4Veou3CGvIx9cGADhTxPO0Cc5gtgx6a3rHgLB1F62u+Bhcb5ALbdQl398Phri/mXTpAthsgpSn65GKnekmre8jUKFj9rnBkdBslLnjV1S0dJ2t1/hB6dirv/lsrv1QwFt0ig8NrFs6z2N+hI1A851XW9Gja32iCRaU/AL4cnpL4dVmF8LUQRMKaoncBrCjB0nW6SiU6Hw4SDdmhSVnDC0dW8pceslZ14vZaoUQkIkg1g5TcYQ/WWweBQpnwT97GpYBrrAlGpp36DY+LGj8e3VVQnhv6ORo5aiPfrSF4Kufn+uUIaaMetph7Xgk=" + secure: "VtbTcUsotXbHcGyVXT0xrErRHt3j104GX0LzuvKEc/uRLGr+Eo3CUHguFSanQ7p2LH2k0DTEmrzMfLVHltXLPmAqvUL3MrM0oUSCf4bkPqcLWiGhHYYtWs4XiYs3aS7esM0SKOyFBHZ1z/Y9JoKIywZgiMD/CI/XqMI5IHfDmy9XAxlqgTN0COp2NWP9SGi2VdJ0sebm7jVGt8kp99yq/2F+VDSADutoVhx68+XMdgd/JMm9Q8ouSra0ni9jpfyN8JJ59ucj8i2LUhz14+zQGcMAYm0QbIhKuibbHsDImLp1vxubAwbvaeA6K5jerZKme8wLAvilLm17bly5RivHER21IW53hccVTRxIzkt3nhJfaU0XBbJ8nxJJJ7F6Rrpzn6Tpcvx4WgODA/0nQ1DRUoGXBiLPO987q7SaYJdP4Ay4dZSTBXt1Rv039IZvTHs0M0MORXB4lSBQ0S0oAJzOA74+jptTW+z+rktXXL8yPBxSIbq7wSPOmrl56gxhojveiXVh6t51+HT16bV4gFRB3SYacDFxSN5ZAGI4ziaBEFYm/PTP5HHciat7qMP7I/8QhV1pkl7IXlENC1I0miYTQN77jzE9Z8RU2rEtYzZBNH5M5agr2AmFrTRu2HD+fINcAyMHvDyrYCnonss7oWjzoJb8PqpkDpSyNsrYGFyi1VU=" cleanup: false on: branch: master From 5bfa71dff37f59a4efc30bc6a36f46dcc49757b0 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Wed, 14 Oct 2020 15:33:32 -0600 Subject: [PATCH 051/151] Fixed hole inlining and added verbose flag --- .gitignore | 1 + examples/svg2mod.svg | 37 ++++++-------- svg2mod/svg/README.md | 2 +- svg2mod/svg/svg/__init__.py | 4 +- svg2mod/svg/svg/svg.py | 37 +++++++------- svg2mod/svg2mod.py | 97 ++++++++++++++++++++++++------------- 6 files changed, 103 insertions(+), 75 deletions(-) diff --git a/.gitignore b/.gitignore index 6509060..2281fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ build dist svg2mod.egg-info *.kicad_mod +*.mod diff --git a/examples/svg2mod.svg b/examples/svg2mod.svg index 6a123e2..7c4fb8f 100644 --- a/examples/svg2mod.svg +++ b/examples/svg2mod.svg @@ -23,9 +23,9 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="1.5673052" - inkscape:cx="431.63967" - inkscape:cy="162.77769" + inkscape:zoom="3.2720174" + inkscape:cx="350.0961" + inkscape:cy="162.60072" inkscape:document-units="mm" inkscape:current-layer="layer3" inkscape:document-rotation="0" @@ -60,7 +60,7 @@ + d="M -0.07560492,0.14291076 H 193.56844 V 59.010697 H -0.07560492 Z" /> @@ -80,9 +80,10 @@ style="fill:#d3bc5f"> + style="fill:#37c837" + d="m 643.54102,50.371094 v 45.724609 19.753907 h -26.88868 l -4.65429,4.65625 -4.65625,4.6543 c -0.1189,-0.0146 -0.23826,-0.0194 -0.35742,-0.0312 -0.0924,-0.009 -0.18463,-0.0179 -0.27735,-0.0254 -0.12457,-0.0101 -0.24827,-0.0262 -0.37305,-0.0332 -0.33709,-0.0189 -0.67587,-0.027 -1.01367,-0.0234 -0.99772,0.0103 -1.98421,0.11814 -2.94726,0.31835 -0.96305,0.20022 -1.90189,0.49365 -2.80469,0.87305 -0.4514,0.1897 -0.89463,0.40033 -1.32617,0.63281 -0.86308,0.46497 -1.68282,1.01456 -2.44727,1.64258 -0.38222,0.31401 -0.75074,0.64724 -1.10351,1 -2.97848,2.97852 -4.4668,6.88331 -4.4668,10.78711 0,3.90381 1.48832,7.80664 4.4668,10.78516 0.33083,0.33084 0.67896,0.64116 1.03515,0.9375 0.0653,0.0544 0.12932,0.11088 0.19532,0.16406 0.11516,0.0926 0.23795,0.17272 0.35546,0.26172 0.33133,0.25146 0.66746,0.49212 1.01368,0.71289 0.14928,0.095 0.30286,0.17992 0.45507,0.26953 0.31919,0.18817 0.64148,0.36832 0.97071,0.53125 0.16774,0.083 0.33729,0.15963 0.50781,0.23633 0.33439,0.15034 0.67139,0.28904 1.01367,0.41406 0.093,0.034 0.18177,0.0791 0.27539,0.11133 0.078,0.0268 0.15989,0.0408 0.23828,0.0664 0.35071,0.11404 0.70397,0.2128 1.06055,0.30078 0.17327,0.0429 0.34471,0.0901 0.51953,0.12695 0.40389,0.0848 0.8104,0.14747 1.21875,0.19922 0.12681,0.0162 0.25157,0.0417 0.37891,0.0547 0.51469,0.0522 1.03198,0.082 1.54883,0.082 0.0195,0 0.039,-0.002 0.0586,-0.002 0.42324,-0.002 0.84839,-0.0252 1.27344,-0.0625 0.0868,-0.008 0.17306,-0.0163 0.25976,-0.0254 0.0906,-0.01 0.1809,-0.0142 0.27149,-0.0254 l 5.98437,5.98438 5.98438,5.98437 h 12.5957 12.5957 l 8.90625,-8.90625 8.90625,-8.90625 v -14.97265 h 15.55078 v -9.38672 -9.38672 H 662.31445 V 99.984375 50.371094 h -9.38672 z m -38.0625,80.542966 c 2.40236,0 4.80377,0.91511 6.63671,2.74805 1.83295,1.83294 2.75,4.23631 2.75,6.63867 10e-6,2.40236 -0.91705,4.80378 -2.75,6.63672 -1.83294,1.83294 -4.23435,2.75 -6.63671,2.75 -2.40237,0 -4.80574,-0.91706 -6.63868,-2.75 -1.83294,-1.83294 -2.75,-4.23435 -2.75,-6.63672 0,-0.30029 0.0143,-0.60121 0.043,-0.90039 0.20048,-2.09428 1.10321,-4.13446 2.70703,-5.73828 1.83295,-1.83294 4.23631,-2.74805 6.63868,-2.74805 z m 18.95117,3.70899 h 19.11133 v 7.19531 l -3.40821,3.4082 -3.4082,3.40821 h -4.81836 -4.81836 l -3.23437,-3.23438 -3.23633,-3.23633 c 0.007,-0.0574 0.009,-0.11453 0.0156,-0.17187 0.008,-0.0677 0.0129,-0.13533 0.0195,-0.20313 0.0488,-0.49766 0.0723,-0.99298 0.0723,-1.48828 2e-5,-0.49557 -0.0234,-0.99229 -0.0723,-1.49023 -0.007,-0.0665 -0.0121,-0.13285 -0.0195,-0.19922 -0.007,-0.0585 -0.008,-0.11727 -0.0156,-0.17578 l 1.90625,-1.90625 z" + transform="matrix(-0.15065532,0.15065532,-0.15065532,-0.15065532,176.82397,-21.691509)" + sodipodi:nodetypes="cccccccsscssssssssccccsssscccccscccccccccccccccccccssssssscssscccccccccccsccccc" /> - + style="font-style:normal;font-weight:normal;font-size:38.4521px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ac9d93;fill-opacity:1"> @@ -124,13 +119,13 @@ style="display:inline;fill:#55d400"> diff --git a/svg2mod/svg/README.md b/svg2mod/svg/README.md index 6ab06cd..b31e322 100644 --- a/svg2mod/svg/README.md +++ b/svg2mod/svg/README.md @@ -2,7 +2,7 @@ SVG parser library ================== This is a SVG parser library written in Python. -([see here](https://github.com/cjlano/svg])) +([see here](https://github.com/svg2mod/svg])) Capabilities: - Parse SVG XML diff --git a/svg2mod/svg/svg/__init__.py b/svg2mod/svg/svg/__init__.py index 3090eb7..94c87db 100644 --- a/svg2mod/svg/svg/__init__.py +++ b/svg2mod/svg/svg/__init__.py @@ -2,7 +2,7 @@ from .svg import * -def parse(filename): - f = svg.Svg(filename) +def parse(filename, verbose=True): + f = svg.Svg(filename, verbose) return f diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index ad155d5..58c2a6d 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -51,9 +51,10 @@ class Transformable: '''Abstract class for objects that can be geometrically drawn & transformed''' - def __init__(self, elt=None): + def __init__(self, elt=None, verbose=True): # a 'Transformable' is represented as a list of Transformable items self.items = [] + self.verbose = verbose self.id = hex(id(self)) # Unit transformation matrix on init self.matrix = Matrix() @@ -94,7 +95,8 @@ def getTransformations(self, elt): op = op.strip() # Keep only numbers arg = [float(x) for x in re.findall(number_re, arg)] - print('transform: ' + op + ' '+ str(arg)) + if self.verbose: + print('transform: ' + op + ' '+ str(arg)) if op == 'matrix': self.matrix *= Matrix(arg) @@ -204,8 +206,8 @@ class Svg(Transformable): # class Svg handles the tag # tag = 'svg' - def __init__(self, filename=None): - Transformable.__init__(self) + def __init__(self, filename=None, verbose=True): + Transformable.__init__(self, verbose=verbose) if filename: self.parse(filename) @@ -217,7 +219,7 @@ def parse(self, filename): raise TypeError('file %s does not seem to be a valid SVG file', filename) # Create a top Group to group all other items (useful for viewBox elt) - top_group = Group() + top_group = Group(verbose=self.verbose) self.items.append(top_group) # SVG dimension @@ -256,8 +258,8 @@ class Group(Transformable): # class Group handles the tag tag = 'g' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, verbose=True): + Transformable.__init__(self, elt, verbose) self.name = "" self.hidden = False @@ -283,10 +285,11 @@ def append(self, element): for elt in element: elt_class = svgClass.get(elt.tag, None) if elt_class is None: - print('No handler for element %s' % elt.tag) + if self.verbose: + print('No handler for element %s' % elt.tag) continue # instanciate elt associated class (e.g. : item = Path(elt) - item = elt_class(elt) + item = elt_class(elt, verbose=self.verbose) # Apply group matrix to the newly created object # Actually, this is effectively done in Svg.__init__() through call to # self.transform(), so doing it here will result in the transformations @@ -357,8 +360,8 @@ class Path(Transformable): # class Path handles the tag tag = 'path' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, verbose=True): + Transformable.__init__(self, elt, verbose) if elt is not None: self.style = elt.get('style') self.parse(elt.get('d')) @@ -537,8 +540,8 @@ class Ellipse(Transformable): # class Ellipse handles the tag tag = 'ellipse' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, verbose=True): + Transformable.__init__(self, elt, verbose) if elt is not None: self.center = Point(self.xlength(elt.get('cx')), self.ylength(elt.get('cy'))) @@ -615,8 +618,8 @@ class Rect(Transformable): # class Rect handles the tag tag = 'rect' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, verbose=True): + Transformable.__init__(self, elt, verbose) if elt is not None: self.P1 = Point(self.xlength(elt.get('x')), self.ylength(elt.get('y'))) @@ -657,8 +660,8 @@ class Line(Transformable): # class Line handles the tag tag = 'line' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, verbose=True): + Transformable.__init__(self, elt, verbose) if elt is not None: self.P1 = Point(self.xlength(elt.get('x1')), self.ylength(elt.get('y1'))) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index c03df4b..c75955b 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -22,7 +22,7 @@ def main(): if pretty: if not use_mm: - print( "Error: decimil units only allowed with legacy output type" ) + print( "Error: decimil units only allowed with legacy output type", file=sys.stderr ) sys.exit( -1 ) #if args.include_reverse: @@ -37,6 +37,7 @@ def main(): args.module_name, args.module_value, args.ignore_hidden_layers, + args.verbose_print, ) # Pick an output file name if none was provided: @@ -63,7 +64,8 @@ def main(): args.scale_factor, args.precision, dpi = args.dpi, - pads = args.convert_to_pads + pads = args.convert_to_pads, + verbose = args.verbose_print, ) else: @@ -80,6 +82,7 @@ def main(): args.scale_factor, args.precision, args.dpi, + verbose = args.verbose_print, ) except Exception as e: @@ -97,6 +100,7 @@ def main(): args.precision, use_mm = use_mm, dpi = args.dpi, + verbose = args.verbose_print, ) # Export the footprint: @@ -222,16 +226,17 @@ class PolygonSegment( object ): #------------------------------------------------------------------------ - def __init__( self, points ): + def __init__( self, points, verbose=True ): self.points = points + self.verbose = verbose if len( points ) < 3: print( "Warning:" " Path segment has only {} points (not a polygon?)".format( len( points ) - ) + ), file=sys.stderr ) @@ -241,29 +246,30 @@ def __init__( self, points ): # and holes within it, so we search for a pair of points connecting the # outline (self) to the hole such that the connecting segment will not # cross the visible inner space within any hole. - def _find_insertion_point( self, hole, holes ): - - #print( " Finding insertion point. {} holes".format( len( holes ) ) ) + def _find_insertion_point( self, hole, holes, other_insertions ): # Try the next point on the container: for cp in range( len( self.points ) ): container_point = self.points[ cp ] - #print( " Trying container point {}".format( cp ) ) - # Try the next point on the hole: for hp in range( len( hole.points ) - 1 ): hole_point = hole.points[ hp ] - #print( " Trying hole point {}".format( cp ) ) - bridge = LineSegment( container_point, hole_point ) + # Check if bridge passes over other bridges that will be created + bad_point = False + for index, insertion in other_insertions: + insert = LineSegment( self.points[index], insertion[0]) + if bridge.intersects(insert): + bad_point = True + if bad_point: + continue + # Check for intersection with each other hole: for other_hole in holes: - #print( " Trying other hole. Check = {}".format( hole == other_hole ) ) - # If the other hole intersects, don't bother checking # remaining holes: if other_hole.intersects( @@ -271,19 +277,19 @@ def _find_insertion_point( self, hole, holes ): check_connects = ( other_hole == hole or other_hole == self ) - ): break - - #print( " Hole does not intersect." ) + ):break else: - print( " Found insertion point: {}, {}".format( cp, hp ) ) + if self.verbose: + print( " Found insertion point: {}, {}".format( cp, hp ) ) # No other holes intersected, so this insertion point # is acceptable: return ( cp, hole.points_starting_on_index( hp ) ) print( - "Could not insert segment without overlapping other segments" + "Could not insert segment without overlapping other segments", + file=sys.stderr ) @@ -317,7 +323,8 @@ def inline( self, segments ): if len( segments ) < 1: return self.points - print( " Inlining {} segments...".format( len( segments ) ) ) + if self.verbose: + print( " Inlining {} segments...".format( len( segments ) ) ) all_segments = segments[ : ] + [ self ] insertions = [] @@ -326,7 +333,7 @@ def inline( self, segments ): for hole in segments: insertion = self._find_insertion_point( - hole, all_segments + hole, all_segments, insertions ) if insertion is not None: insertions.append( insertion ) @@ -452,22 +459,24 @@ def _prune_hidden( self, items = None ): if not isinstance( item, svg.Group ): continue - if( item.hidden ): - print("Ignoring hidden SVG layer: {}".format( item.name ) ) - else: + if item.hidden : + if self.verbose: + print("Ignoring hidden SVG layer: {}".format( item.name ) ) + elif item.name is not "": self.svg.items.append( item ) if(item.items): self._prune_hidden( item.items ) - def __init__( self, file_name, module_name, module_value, ignore_hidden_layers ): + def __init__( self, file_name, module_name, module_value, ignore_hidden_layers, verbose_print ): self.file_name = file_name self.module_name = module_name self.module_value = module_value + self.verbose = verbose_print print( "Parsing SVG..." ) - self.svg = svg.parse( file_name ) + self.svg = svg.parse( file_name, verbose_print ) if( ignore_hidden_layers ): self._prune_hidden() @@ -478,6 +487,8 @@ def __init__( self, file_name, module_name, module_value, ignore_hidden_layers ) class Svg2ModExport( object ): + verbose = True + #------------------------------------------------------------------------ @staticmethod @@ -542,6 +553,7 @@ def __init__( use_mm = True, dpi = DEFAULT_DPI, pads = False, + verbose = True ): if use_mm: # 25.4 mm/in; @@ -559,6 +571,7 @@ def __init__( self.use_mm = use_mm self.dpi = dpi self.convert_pads = pads + self.verbose = verbose #------------------------------------------------------------------------ @@ -603,7 +616,7 @@ def _prune( self, items = None ): for name in self.layers.keys(): #if re.search( name, item.name, re.I ): - if name == item.name: + if name == item.name and item.name is not "": print( "Found SVG layer: {}".format( item.name ) ) self.imported.svg.items.append( item ) @@ -626,7 +639,7 @@ def _write_items( self, items, layer, flip = False ): elif isinstance( item, svg.Path ): segments = [ - PolygonSegment( segment ) + PolygonSegment( segment, verbose=self.verbose ) for segment in item.segments( precision = self.precision ) @@ -648,9 +661,10 @@ def _write_items( self, items, layer, flip = False ): stroke_width ) - print( " Writing polygon with {} points".format( - len( points ) ) - ) + if self.verbose: + print( " Writing polygon with {} points".format( + len( points ) ) + ) self._write_polygon( points, layer, fill, stroke, stroke_width @@ -659,7 +673,7 @@ def _write_items( self, items, layer, flip = False ): else: print( "Unsupported SVG element: {}".format( item.__class__.__name__ - ) ) + ), file=sys.stderr) #------------------------------------------------------------------------ @@ -810,6 +824,7 @@ def __init__( precision = 20.0, use_mm = True, dpi = DEFAULT_DPI, + verbose = True, ): super( Svg2ModExportLegacy, self ).__init__( svg2mod_import, @@ -819,6 +834,8 @@ def __init__( precision, use_mm, dpi, + pads = False, + verbose = verbose ) self.include_reverse = True @@ -1002,6 +1019,7 @@ def __init__( precision = 20.0, dpi = DEFAULT_DPI, include_reverse = True, + verbose = True ): self.file_name = file_name use_mm = self._parse_output_file() @@ -1014,6 +1032,7 @@ def __init__( precision, use_mm, dpi, + verbose = verbose, ) @@ -1021,7 +1040,8 @@ def __init__( def _parse_output_file( self ): - print( "Parsing module file: {}".format( self.file_name ) ) + if self.verbose: + print( "Parsing module file: {}".format( self.file_name ) ) module_file = open( self.file_name, 'r' ) lines = module_file.readlines() module_file.close() @@ -1044,7 +1064,6 @@ def _parse_output_file( self ): m = re.match( "Units[\s]+mm[\s]*", line ) if m is not None: - print( " Use mm detected" ) use_mm = True # Read the index: @@ -1102,7 +1121,8 @@ def _read_module( self, lines, index ): m = re.match( r'\$MODULE[\s]+([^\s]+)[\s]*', lines[ index ] ) module_name = m.group( 1 ) - print( " Reading module {}".format( module_name ) ) + if self.verbose: + print( " Reading module {}".format( module_name ) ) index += 1 module_lines = [] @@ -1478,6 +1498,15 @@ def get_arguments(): default = False, ) + parser.add_argument( + '-v', '--verbose', + dest = 'verbose_print', + action = 'store_const', + const = True, + help = "Print more verbose messages", + default = False, + ) + parser.add_argument( '-x', '--exclude-hidden', dest = 'ignore_hidden_layers', From 86e54b573b106191a36b4eea78065e6fce3f7d64 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Wed, 14 Oct 2020 19:35:04 -0600 Subject: [PATCH 052/151] Add legacy testing --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2780edf..487a3de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,9 @@ jobs: language: python script: - pip3 install ./ - - svg2mod -i examples/svg2mod.svg -o output.kicad_mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format pretty --units mm -d 300 + - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 -v + - svg2mod -i examples/svg2mod.svg -v + - svg2mod -i examples/svg2mod.svg --format legacy - svg2mod -i examples/svg2mod.svg - stage: deploy os: linux From fc8676c9551e433681701b226ec22ef0a4b0ad08 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Wed, 14 Oct 2020 20:11:49 -0600 Subject: [PATCH 053/151] Update documentation --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb5ab85..d200e15 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Python 3 ## Usage ``` -usage: svg2mod [-h] -i FILENAME [-o FILENAME] [-c] [-pads] [-x] [-d DPI] +usage: svg2mod [-h] -i FILENAME [-o FILENAME] [-c] [-pads] [-v] [-x] [-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] [--name NAME] [--units UNITS] [--value VALUE] @@ -42,6 +42,7 @@ optional arguments: -c, --center Center the module to the center of the bounding box -pads, --convert-pads Convert any artwork on Cu layers to pads + -v, --verbose Print more verbose messages -x, --exclude-hidden Do not export hidden layers -d DPI, --dpi DPI DPI of the SVG file (int) -f FACTOR, --factor FACTOR From e164f7d3b518256044bac09243a09e940b58c383 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Wed, 14 Oct 2020 21:34:09 -0600 Subject: [PATCH 054/151] Allow travis to upload bdist_wheel --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 487a3de..48494c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ jobs: username: "__token__" password: secure: "VtbTcUsotXbHcGyVXT0xrErRHt3j104GX0LzuvKEc/uRLGr+Eo3CUHguFSanQ7p2LH2k0DTEmrzMfLVHltXLPmAqvUL3MrM0oUSCf4bkPqcLWiGhHYYtWs4XiYs3aS7esM0SKOyFBHZ1z/Y9JoKIywZgiMD/CI/XqMI5IHfDmy9XAxlqgTN0COp2NWP9SGi2VdJ0sebm7jVGt8kp99yq/2F+VDSADutoVhx68+XMdgd/JMm9Q8ouSra0ni9jpfyN8JJ59ucj8i2LUhz14+zQGcMAYm0QbIhKuibbHsDImLp1vxubAwbvaeA6K5jerZKme8wLAvilLm17bly5RivHER21IW53hccVTRxIzkt3nhJfaU0XBbJ8nxJJJ7F6Rrpzn6Tpcvx4WgODA/0nQ1DRUoGXBiLPO987q7SaYJdP4Ay4dZSTBXt1Rv039IZvTHs0M0MORXB4lSBQ0S0oAJzOA74+jptTW+z+rktXXL8yPBxSIbq7wSPOmrl56gxhojveiXVh6t51+HT16bV4gFRB3SYacDFxSN5ZAGI4ziaBEFYm/PTP5HHciat7qMP7I/8QhV1pkl7IXlENC1I0miYTQN77jzE9Z8RU2rEtYzZBNH5M5agr2AmFrTRu2HD+fINcAyMHvDyrYCnonss7oWjzoJb8PqpkDpSyNsrYGFyi1VU=" - cleanup: false + distributions: "sdist bdist_wheel" on: branch: master tags: true From 4be236fbd3d2594fbc2724a18dfb6239efaf47cb Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Thu, 15 Oct 2020 00:01:58 -0600 Subject: [PATCH 055/151] Small changes to make documentation clearer --- setup.py | 1 + svg2mod/svg/README.md | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 9e6b6f3..3166833 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ 'Intended Audience :: Science/Research', 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', 'Natural Language :: English', + 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', ], test_suite='tests', diff --git a/svg2mod/svg/README.md b/svg2mod/svg/README.md index b31e322..ae4ce0d 100644 --- a/svg2mod/svg/README.md +++ b/svg2mod/svg/README.md @@ -1,8 +1,9 @@ SVG parser library ================== -This is a SVG parser library written in Python. -([see here](https://github.com/svg2mod/svg])) +This is a SVG parser library written in Python and is currently only developed to support +[svg2mod](https://github.com/svg2mod/svg2mod). + Capabilities: - Parse SVG XML From 664ee868d087932c03c32a7a5836f2ab4d9a2830 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Thu, 22 Oct 2020 22:48:25 -0600 Subject: [PATCH 056/151] Fix verbose flag and stroke width calulation Fixed verbose flag by adding verbose to the constructor of circle. Fixed stroke width calculation for non default scaling by calculating the documents units per pixel. Added filtering for fill and stroke to remove them if they are completely transparent in the svg. --- svg2mod/svg/svg/svg.py | 15 +++++++++------ svg2mod/svg2mod.py | 13 +++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 58c2a6d..8079e64 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -207,6 +207,7 @@ class Svg(Transformable): # tag = 'svg' def __init__(self, filename=None, verbose=True): + viewport_scale = 1 Transformable.__init__(self, verbose=verbose) if filename: self.parse(filename) @@ -235,6 +236,7 @@ def parse(self, filename): sy = height / float(viewBox[3]) tx = -float(viewBox[0]) ty = -float(viewBox[1]) + self.viewport_scale = round(float(viewBox[2])/width, 6) top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) # Parse XML elements hierarchically with groups @@ -482,22 +484,23 @@ def parse(self, pathstr): flags = pathlst.pop().strip() large_arc_flag = flags[0] if large_arc_flag not in '01': - print('Arc parsing failure') + print('Arc parsing failure', file=sys.error) break if len(flags) > 1: flags = flags[1:].strip() else: flags = pathlst.pop().strip() sweep_flag = flags[0] if sweep_flag not in '01': - print('Arc parsing failure') + print('Arc parsing failure', file=sys.error) break if len(flags) > 1: x = flags[1:] else: x = pathlst.pop() y = pathlst.pop() # TODO - print('ARC: ' + - ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) + if self.verbose: + print('ARC: ' + + ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) # self.items.append( # Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) @@ -604,11 +607,11 @@ class Circle(Ellipse): # class Circle handles the tag tag = 'circle' - def __init__(self, elt=None): + def __init__(self, elt=None, verbose=True): if elt is not None: elt.set('rx', elt.get('r')) elt.set('ry', elt.get('r')) - Ellipse.__init__(self, elt) + Ellipse.__init__(self, elt, verbose=verbose) def __repr__(self): return '' diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index c75955b..ac3a806 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -477,6 +477,8 @@ def __init__( self, file_name, module_name, module_value, ignore_hidden_layers, print( "Parsing SVG..." ) self.svg = svg.parse( file_name, verbose_print ) + if verbose_print: + print("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) if( ignore_hidden_layers ): self._prune_hidden() @@ -522,6 +524,10 @@ def _get_fill_stroke( self, item ): if name == "fill" and value == "none": fill = False + elif name == "fill-opacity": + if float(value) == 0: + fill = False + elif name == "stroke" and value == "none": stroke = False @@ -532,6 +538,13 @@ def _get_fill_stroke( self, item ): else: stroke_width = float( value ) + # units per pixel converted to mm + scale = self.imported.svg.viewport_scale* DEFAULT_DPI / 25.4 + stroke_width = round(stroke_width/scale, 6) # remove unessesary presion to reduce floating point errors + elif name == "stroke-opacity": + if float(value) == 0: + stroke = False + if not stroke: stroke_width = 0.0 elif stroke_width is None: From 277c5319ca7b29a45197f9355b8f1e324c6c04df Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Fri, 23 Oct 2020 11:42:18 -0600 Subject: [PATCH 057/151] Added support for Rect and Cricle elements --- README.md | 5 +++-- svg2mod/svg/svg/svg.py | 16 +++++++++------- svg2mod/svg2mod.py | 15 +++++++++++---- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d200e15..132e65a 100644 --- a/README.md +++ b/README.md @@ -62,13 +62,14 @@ optional arguments: svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. * 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. - * Paths are supported. + * Paths, Rect, and Circles (Ellipse) are supported. * A path may have an outline and a fill. (Colors will be ignored.) * A path may have holes, defined by interior segments within the path (see included examples). * A path with a filled area inside a hole will not work properly. You must split these apart into two seperate paths. + * Transparent fills and strokes with be ignored * 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 named to match the target in kicad. The supported layers are listed below. - * Other types of elements such as rect, arc, and circle are not supported. + * Other types of elements such as arc 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. ### Layers diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 8079e64..a7123f1 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -562,9 +562,10 @@ def bbox(self): return (pmin, pmax) def transform(self, matrix): - self.center = self.matrix * self.center - self.rx = self.matrix.xlength(self.rx) - self.ry = self.matrix.ylength(self.ry) + self.center = matrix * self.center + self.rx = matrix.xlength(self.rx) + self.ry = matrix.ylength(self.ry) + def scale(self, ratio): self.center *= ratio @@ -629,6 +630,7 @@ def __init__(self, elt=None, verbose=True): self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), self.P1.y + self.ylength(elt.get('height'))) + self.style = elt.get('style') def __repr__(self): return '' @@ -643,8 +645,8 @@ def bbox(self): return (Point(xmin,ymin), Point(xmax,ymax)) def transform(self, matrix): - self.P1 = self.matrix * self.P1 - self.P2 = self.matrix * self.P2 + self.P1 = matrix * self.P1 + self.P2 = matrix * self.P2 def segments(self, precision=0): # A rectangle is built with a segment going thru 4 points @@ -685,8 +687,8 @@ def bbox(self): return (Point(xmin,ymin), Point(xmax,ymax)) def transform(self, matrix): - self.P1 = self.matrix * self.P1 - self.P2 = self.matrix * self.P2 + self.P1 = matrix * self.P1 + self.P2 = matrix * self.P2 self.segment = Segment(self.P1, self.P2) def segments(self, precision=0): diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index ac3a806..79fbf7f 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -534,13 +534,16 @@ def _get_fill_stroke( self, item ): elif name == "stroke-width": if value.endswith("px"): value = value.replace( "px", "" ) - stroke_width = float( value ) * 25.4 / float(self.dpi) + stroke_width = float( value ) else: stroke_width = float( value ) # units per pixel converted to mm - scale = self.imported.svg.viewport_scale* DEFAULT_DPI / 25.4 - stroke_width = round(stroke_width/scale, 6) # remove unessesary presion to reduce floating point errors + scale = self.imported.svg.viewport_scale * float(self.dpi) / 25.4 + + # remove unessesary presion to reduce floating point errors + stroke_width = round(stroke_width/scale, 6) + elif name == "stroke-opacity": if float(value) == 0: stroke = False @@ -649,7 +652,11 @@ def _write_items( self, items, layer, flip = False ): self._write_items( item.items, layer, flip ) continue - elif isinstance( item, svg.Path ): + elif ( + isinstance( item, svg.Path ) or + isinstance( item, svg.Ellipse) or + isinstance( item, svg.Rect ) + ): segments = [ PolygonSegment( segment, verbose=self.verbose ) From 7be2f638217a31ee8582b3f567439ce0b2a2371c Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Fri, 23 Oct 2020 22:53:09 -0600 Subject: [PATCH 058/151] Fixed crash and added support for rect rotation Fixed a crash caused by empty paths and added support for rect rotation. --- svg2mod/svg/svg/svg.py | 51 ++++++++++++++++++++++++++++++++++-------- svg2mod/svg2mod.py | 37 +++++++++++++++++------------- 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index a7123f1..3248036 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -58,6 +58,7 @@ def __init__(self, elt=None, verbose=True): self.id = hex(id(self)) # Unit transformation matrix on init self.matrix = Matrix() + self.rotation = 0 self.viewport = Point(800, 600) # default viewport is 800x600 if elt is not None: self.id = elt.get('id', self.id) @@ -114,6 +115,7 @@ def getTransformations(self, elt): self.matrix *= Matrix([sx, 0, 0, sy, 0, 0]) if op == 'rotate': + self.rotation += arg[0] cosa = math.cos(math.radians(arg[0])) sina = math.sin(math.radians(arg[0])) if len(arg) != 1: @@ -484,14 +486,14 @@ def parse(self, pathstr): flags = pathlst.pop().strip() large_arc_flag = flags[0] if large_arc_flag not in '01': - print('Arc parsing failure', file=sys.error) + print('\033[91mArc parsing failure\033[0m', file=sys.error) break if len(flags) > 1: flags = flags[1:].strip() else: flags = pathlst.pop().strip() sweep_flag = flags[0] if sweep_flag not in '01': - print('Arc parsing failure', file=sys.error) + print('\033[91mArc parsing failure\033[0m', file=sys.error) break if len(flags) > 1: x = flags[1:] @@ -499,8 +501,10 @@ def parse(self, pathstr): y = pathlst.pop() # TODO if self.verbose: - print('ARC: ' + - ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) + print('\033[91mUnsupported ARC: ' + + ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y]) + "\033[0m", + file=sys.stderr + ) # self.items.append( # Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) @@ -561,12 +565,15 @@ def bbox(self): pmax = self.center + Point(self.rx, self.ry) return (pmin, pmax) - def transform(self, matrix): + def transform(self, matrix=None): + if matrix is None: + matrix = self.matrix + else: + matrix = self.matrix * matrix self.center = matrix * self.center self.rx = matrix.xlength(self.rx) self.ry = matrix.ylength(self.ry) - def scale(self, ratio): self.center *= ratio self.rx *= ratio @@ -583,6 +590,13 @@ def P(self, t): return Point(x,y) def segments(self, precision=0): + if self.verbose and self.rotation % 180 != 0: + print( + "\033[91mUnsupported rotation for {} primitive\033[0m".format( + self.__class__.__name__ + ), + file=sys.stderr + ) if max(self.rx, self.ry) < precision: return [[self.center]] @@ -631,6 +645,8 @@ def __init__(self, elt=None, verbose=True): self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), self.P1.y + self.ylength(elt.get('height'))) self.style = elt.get('style') + if self.verbose and (elt.get('rx') or elt.get('ry')): + print("\033[91mUnsupported corner radius on rect.\033[0m", file=sys.stderr) def __repr__(self): return '' @@ -644,15 +660,32 @@ def bbox(self): return (Point(xmin,ymin), Point(xmax,ymax)) - def transform(self, matrix): + def transform(self, matrix=None): + if matrix is None: + matrix = self.matrix + else: + matrix = self.matrix*matrix self.P1 = matrix * self.P1 self.P2 = matrix * self.P2 def segments(self, precision=0): # A rectangle is built with a segment going thru 4 points ret = [] - Pa = Point(self.P1.x, self.P2.y) - Pb = Point(self.P2.x, self.P1.y) + Pa, Pb = Point(0,0),Point(0,0) + if self.rotation % 90 == 0: + Pa = Point(self.P1.x, self.P2.y) + Pb = Point(self.P2.x, self.P1.y) + else: + sa = math.sin(math.radians(self.rotation)) / math.cos(math.radians(self.rotation)) + sb = -1 / sa + ba = -sa * self.P1.x + self.P1.y + bb = -sb * self.P2.x + self.P2.y + x = (ba-bb) / (sb-sa) + Pa = Point(x, sa * x + ba) + bb = -sb * self.P1.x + self.P1.y + ba = -sa * self.P2.x + self.P2.y + x = (ba-bb) / (sb-sa) + Pb = Point(x, sa * x + ba) ret.append([self.P1, Pa, self.P2, Pb, self.P1]) return ret diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 79fbf7f..93cbed8 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -22,7 +22,7 @@ def main(): if pretty: if not use_mm: - print( "Error: decimil units only allowed with legacy output type", file=sys.stderr ) + print( "\033[91mError: decimil units only allowed with legacy output type\033[0m", file=sys.stderr ) sys.exit( -1 ) #if args.include_reverse: @@ -233,8 +233,8 @@ def __init__( self, points, verbose=True ): if len( points ) < 3: print( - "Warning:" - " Path segment has only {} points (not a polygon?)".format( + "\033[91mWarning:" + " Path segment has only {} points (not a polygon?)\033[0m".format( len( points ) ), file=sys.stderr ) @@ -288,7 +288,7 @@ def _find_insertion_point( self, hole, holes, other_insertions ): return ( cp, hole.points_starting_on_index( hp ) ) print( - "Could not insert segment without overlapping other segments", + "\033[91mCould not insert segment without overlapping other segments\033[0m", file=sys.stderr ) @@ -676,22 +676,27 @@ def _write_items( self, items, layer, flip = False ): elif len( segments ) > 0: points = segments[ 0 ].points - if not self.use_mm: - stroke_width = self._convert_mm_to_decimil( - stroke_width - ) + if len ( segments ) != 0: + if not self.use_mm: + stroke_width = self._convert_mm_to_decimil( + stroke_width + ) - if self.verbose: - print( " Writing polygon with {} points".format( - len( points ) ) - ) + if self.verbose: + print( " Writing polygon with {} points".format( + len( points ) ) + ) - self._write_polygon( - points, layer, fill, stroke, stroke_width - ) + self._write_polygon( + points, layer, fill, stroke, stroke_width + ) + elif self.verbose: + print( "\033[91mSkipping {} with 0 points\033[0m".format( + item.__class__.__name__ + ), file=sys.stderr) else: - print( "Unsupported SVG element: {}".format( + print( "\033[91mUnsupported SVG element: {}\033[0m".format( item.__class__.__name__ ), file=sys.stderr) From 2114502dfd78842c685e5b7391d9edbbe6401cf8 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Fri, 23 Oct 2020 23:25:31 -0600 Subject: [PATCH 059/151] Fix documentation --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 132e65a..68b0ee3 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,13 @@ optional arguments: svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. * 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. - * Paths, Rect, and Circles (Ellipse) are supported. + * Paths are fully supported Rect and Circles (Ellipse) are parially supported. * A path may have an outline and a fill. (Colors will be ignored.) * A path may have holes, defined by interior segments within the path (see included examples). * A path with a filled area inside a hole will not work properly. You must split these apart into two seperate paths. - * Transparent fills and strokes with be ignored + * Transparent fills and strokes with be ignored. + * Rect supports rotations, but not corner radii. + * Ellipses do not support rotation. * 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 named to match the target in kicad. The supported layers are listed below. * Other types of elements such as arc are not supported. From a9915ddbd68663f8c37ca5ffb108b756681e3b10 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen <42423149+Sodium-Hydrogen@users.noreply.github.com> Date: Fri, 23 Oct 2020 23:27:00 -0600 Subject: [PATCH 060/151] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68b0ee3..e9f7731 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ optional arguments: svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. * 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. - * Paths are fully supported Rect and Circles (Ellipse) are parially supported. + * Paths are fully supported Rect and Circles (Ellipse) are partially supported. * A path may have an outline and a fill. (Colors will be ignored.) * A path may have holes, defined by interior segments within the path (see included examples). * A path with a filled area inside a hole will not work properly. You must split these apart into two seperate paths. From 678b53da942674fa2001f362766f9c2f896ab6be Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 26 Oct 2020 10:51:38 -0600 Subject: [PATCH 061/151] bug fix for #17 --- svg2mod/svg2mod.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 93cbed8..e334608 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -532,11 +532,7 @@ def _get_fill_stroke( self, item ): stroke = False elif name == "stroke-width": - if value.endswith("px"): - value = value.replace( "px", "" ) - stroke_width = float( value ) - else: - stroke_width = float( value ) + stroke_width = float( "".join(i for i in value if not i.isalpha()) ) # units per pixel converted to mm scale = self.imported.svg.viewport_scale * float(self.dpi) / 25.4 From a0a398660298920720e7f764fac2ede8efa294e4 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Thu, 19 Nov 2020 23:11:06 -0700 Subject: [PATCH 062/151] Bug fix for #19 If height or width properties cannot be found svg2mod will now attempt to fallback to viewbox sizing. If that also doesn't exists svg2mod will no longer continue execution. --- svg2mod/svg/svg/svg.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 3248036..6a53db0 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -228,18 +228,31 @@ def parse(self, filename): # SVG dimension width = self.xlength(self.root.get('width')) height = self.ylength(self.root.get('height')) + # update viewport top_group.viewport = Point(width, height) # viewBox if self.root.get('viewBox') is not None: viewBox = re.findall(number_re, self.root.get('viewBox')) + + # If the document somehow doesn't have dimentions get if from viewBox + if self.root.get('width') is None or self.root.get('height') is None: + width = float(viewBox[2]) + height = float(viewBox[3]) + if self.verbose: + print("\033[91mUnable to find width of height properties. Falling back to viewBox.\033[0m", file=sys.stderr) + sx = width / float(viewBox[2]) sy = height / float(viewBox[3]) tx = -float(viewBox[0]) ty = -float(viewBox[1]) self.viewport_scale = round(float(viewBox[2])/width, 6) top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) + if ( self.root.get("width") is None or self.root.get("height") is None ) \ + and self.root.get("viewBox") is None: + print("\033[91mFatal Error: Unable to find SVG dimensions. Exiting.\033[0m", file=sys.stderr) + exit() # Parse XML elements hierarchically with groups top_group.append(self.root) From 7219610e026e42c9dd356c0041d157cbd76a110b Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 13 Jan 2021 23:44:16 -0700 Subject: [PATCH 063/151] Migrated output to be used via python logging This allowed for a more granular control over log messages as well as making it easier to have messages formated without having to work directly with ANSI terminal codes --- setup.py | 2 +- svg2mod/coloredlogger.py | 21 +++++++ svg2mod/svg/svg/__init__.py | 4 +- svg2mod/svg/svg/svg.py | 72 +++++++++------------- svg2mod/svg2mod.py | 120 +++++++++++++++++++----------------- 5 files changed, 117 insertions(+), 102 deletions(-) create mode 100644 svg2mod/coloredlogger.py diff --git a/setup.py b/setup.py index 3166833..8e88bf8 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ try: tag = os.popen("git describe --tag")._stream.read().strip() except: - tag = "develpment" + tag = "development" requirements = [ ] diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py new file mode 100644 index 0000000..7be942e --- /dev/null +++ b/svg2mod/coloredlogger.py @@ -0,0 +1,21 @@ +import logging + + +class Formatter(logging.Formatter): + color = { + logging.CRITICAL: "\033[91m\033[7m", #Set red and swap background and foreground + logging.ERROR: "\033[91m", #Set red + logging.WARNING: "\033[93m", #Set yellow + logging.DEBUG: "\033[90m", #Set dark gray/black + logging.INFO: "" #Do nothing + } + reset = "\033[0m" + def __init__(self, fmt="%(message)s", datefmt=None, style="%"): + super().__init__(fmt, datefmt, style) + + def format(self, record): + fmt_org = self._style._fmt + self._style._fmt = Formatter.color[record.levelno] + fmt_org + Formatter.reset + result = logging.Formatter.format(self, record) + self._style._fmt = fmt_org + return result \ No newline at end of file diff --git a/svg2mod/svg/svg/__init__.py b/svg2mod/svg/svg/__init__.py index 94c87db..3090eb7 100644 --- a/svg2mod/svg/svg/__init__.py +++ b/svg2mod/svg/svg/__init__.py @@ -2,7 +2,7 @@ from .svg import * -def parse(filename, verbose=True): - f = svg.Svg(filename, verbose) +def parse(filename): + f = svg.Svg(filename) return f diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 6a53db0..9fcc541 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -26,6 +26,7 @@ import itertools import operator import json +import logging from .geometry import * @@ -51,10 +52,9 @@ class Transformable: '''Abstract class for objects that can be geometrically drawn & transformed''' - def __init__(self, elt=None, verbose=True): + def __init__(self, elt=None): # a 'Transformable' is represented as a list of Transformable items self.items = [] - self.verbose = verbose self.id = hex(id(self)) # Unit transformation matrix on init self.matrix = Matrix() @@ -96,8 +96,7 @@ def getTransformations(self, elt): op = op.strip() # Keep only numbers arg = [float(x) for x in re.findall(number_re, arg)] - if self.verbose: - print('transform: ' + op + ' '+ str(arg)) + logging.debug('transform: ' + op + ' '+ str(arg)) if op == 'matrix': self.matrix *= Matrix(arg) @@ -208,9 +207,9 @@ class Svg(Transformable): # class Svg handles the tag # tag = 'svg' - def __init__(self, filename=None, verbose=True): + def __init__(self, filename=None): viewport_scale = 1 - Transformable.__init__(self, verbose=verbose) + Transformable.__init__(self) if filename: self.parse(filename) @@ -222,7 +221,7 @@ def parse(self, filename): raise TypeError('file %s does not seem to be a valid SVG file', filename) # Create a top Group to group all other items (useful for viewBox elt) - top_group = Group(verbose=self.verbose) + top_group = Group() self.items.append(top_group) # SVG dimension @@ -240,8 +239,7 @@ def parse(self, filename): if self.root.get('width') is None or self.root.get('height') is None: width = float(viewBox[2]) height = float(viewBox[3]) - if self.verbose: - print("\033[91mUnable to find width of height properties. Falling back to viewBox.\033[0m", file=sys.stderr) + logging.warning("Unable to find width of height properties. Falling back to viewBox.") sx = width / float(viewBox[2]) sy = height / float(viewBox[3]) @@ -251,8 +249,8 @@ def parse(self, filename): top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) if ( self.root.get("width") is None or self.root.get("height") is None ) \ and self.root.get("viewBox") is None: - print("\033[91mFatal Error: Unable to find SVG dimensions. Exiting.\033[0m", file=sys.stderr) - exit() + logging.critical("Fatal Error: Unable to find SVG dimensions. Exiting.") + sys.exit(-1) # Parse XML elements hierarchically with groups top_group.append(self.root) @@ -275,8 +273,8 @@ class Group(Transformable): # class Group handles the tag tag = 'g' - def __init__(self, elt=None, verbose=True): - Transformable.__init__(self, elt, verbose) + def __init__(self, elt=None): + Transformable.__init__(self, elt) self.name = "" self.hidden = False @@ -302,11 +300,10 @@ def append(self, element): for elt in element: elt_class = svgClass.get(elt.tag, None) if elt_class is None: - if self.verbose: - print('No handler for element %s' % elt.tag) + logging.warning('No handler for element %s' % elt.tag) continue # instanciate elt associated class (e.g. : item = Path(elt) - item = elt_class(elt, verbose=self.verbose) + item = elt_class(elt) # Apply group matrix to the newly created object # Actually, this is effectively done in Svg.__init__() through call to # self.transform(), so doing it here will result in the transformations @@ -377,8 +374,8 @@ class Path(Transformable): # class Path handles the tag tag = 'path' - def __init__(self, elt=None, verbose=True): - Transformable.__init__(self, elt, verbose) + def __init__(self, elt=None): + Transformable.__init__(self, elt) if elt is not None: self.style = elt.get('style') self.parse(elt.get('d')) @@ -499,25 +496,21 @@ def parse(self, pathstr): flags = pathlst.pop().strip() large_arc_flag = flags[0] if large_arc_flag not in '01': - print('\033[91mArc parsing failure\033[0m', file=sys.error) + logging.error("Arc parsing failure") break if len(flags) > 1: flags = flags[1:].strip() else: flags = pathlst.pop().strip() sweep_flag = flags[0] if sweep_flag not in '01': - print('\033[91mArc parsing failure\033[0m', file=sys.error) + logging.error("Arc parsing failure") break if len(flags) > 1: x = flags[1:] else: x = pathlst.pop() y = pathlst.pop() # TODO - if self.verbose: - print('\033[91mUnsupported ARC: ' + - ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y]) + "\033[0m", - file=sys.stderr - ) + logging.warning("Unsupported ARC: , ".join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) # self.items.append( # Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) @@ -560,8 +553,8 @@ class Ellipse(Transformable): # class Ellipse handles the tag tag = 'ellipse' - def __init__(self, elt=None, verbose=True): - Transformable.__init__(self, elt, verbose) + def __init__(self, elt=None): + Transformable.__init__(self, elt) if elt is not None: self.center = Point(self.xlength(elt.get('cx')), self.ylength(elt.get('cy'))) @@ -603,13 +596,8 @@ def P(self, t): return Point(x,y) def segments(self, precision=0): - if self.verbose and self.rotation % 180 != 0: - print( - "\033[91mUnsupported rotation for {} primitive\033[0m".format( - self.__class__.__name__ - ), - file=sys.stderr - ) + if self.rotation % 180 != 0: + logging.warning("Unsupported rotation for {} primitive".format(self.__class__.__name__)) if max(self.rx, self.ry) < precision: return [[self.center]] @@ -635,11 +623,11 @@ class Circle(Ellipse): # class Circle handles the tag tag = 'circle' - def __init__(self, elt=None, verbose=True): + def __init__(self, elt=None): if elt is not None: elt.set('rx', elt.get('r')) elt.set('ry', elt.get('r')) - Ellipse.__init__(self, elt, verbose=verbose) + Ellipse.__init__(self, elt) def __repr__(self): return '' @@ -649,8 +637,8 @@ class Rect(Transformable): # class Rect handles the tag tag = 'rect' - def __init__(self, elt=None, verbose=True): - Transformable.__init__(self, elt, verbose) + def __init__(self, elt=None): + Transformable.__init__(self, elt) if elt is not None: self.P1 = Point(self.xlength(elt.get('x')), self.ylength(elt.get('y'))) @@ -658,8 +646,8 @@ def __init__(self, elt=None, verbose=True): self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), self.P1.y + self.ylength(elt.get('height'))) self.style = elt.get('style') - if self.verbose and (elt.get('rx') or elt.get('ry')): - print("\033[91mUnsupported corner radius on rect.\033[0m", file=sys.stderr) + if (elt.get('rx') or elt.get('ry')): + logging.warning("Unsupported corner radius on rect.") def __repr__(self): return '' @@ -711,8 +699,8 @@ class Line(Transformable): # class Line handles the tag tag = 'line' - def __init__(self, elt=None, verbose=True): - Transformable.__init__(self, elt, verbose) + def __init__(self, elt=None): + Transformable.__init__(self, elt) if elt is not None: self.P1 = Point(self.xlength(elt.get('x1')), self.ylength(elt.get('y1'))) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index e334608..0251368 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1,11 +1,11 @@ -#!/usr/bin/env python3 import argparse import datetime import os -from pprint import pformat, pprint import re import svg2mod.svg as svg +import logging +import svg2mod.coloredlogger as coloredlogger import sys @@ -19,10 +19,41 @@ def main(): pretty = args.format == 'pretty' use_mm = args.units == 'mm' + + + # Setup root logger to use terminal colored outputs as well as stdout and stderr + error_handler = logging.StreamHandler(sys.stderr) + error_handler.setFormatter(coloredlogger.Formatter()) + error_handler.addFilter(lambda record: logging.WARNING <= record.levelno) # Send logs with level ERROR and above to stderr + + log_handler = logging.StreamHandler(sys.stdout) + log_handler.setFormatter(coloredlogger.Formatter()) + log_handler.addFilter(lambda record: logging.WARNING > record.levelno) # Send logs with level INFO and below to stdout + + logging.root.addHandler(error_handler) + logging.root.addHandler(log_handler) + + if args.verbose_print: + logging.root.setLevel(logging.INFO) + elif args.debug_print: + logging.root.setLevel(logging.DEBUG) + else: + logging.root.setLevel(logging.ERROR) + + # Add a second logger that will bypass the log level and output anyway + # It is a good practice to send only messages level INFO via this logger + logging.getLogger("unfiltered").setLevel(logging.INFO) + + # This can be used sparingly as follows: + ''' + logging.getLogger("unfiltered").info("Message Here") + ''' + + if pretty: if not use_mm: - print( "\033[91mError: decimil units only allowed with legacy output type\033[0m", file=sys.stderr ) + logging.critical("Error: decimil units only allowed with legacy output type") sys.exit( -1 ) #if args.include_reverse: @@ -37,7 +68,6 @@ def main(): args.module_name, args.module_value, args.ignore_hidden_layers, - args.verbose_print, ) # Pick an output file name if none was provided: @@ -65,7 +95,6 @@ def main(): args.precision, dpi = args.dpi, pads = args.convert_to_pads, - verbose = args.verbose_print, ) else: @@ -82,7 +111,6 @@ def main(): args.scale_factor, args.precision, args.dpi, - verbose = args.verbose_print, ) except Exception as e: @@ -100,7 +128,6 @@ def main(): args.precision, use_mm = use_mm, dpi = args.dpi, - verbose = args.verbose_print, ) # Export the footprint: @@ -226,18 +253,12 @@ class PolygonSegment( object ): #------------------------------------------------------------------------ - def __init__( self, points, verbose=True ): + def __init__( self, points): self.points = points - self.verbose = verbose if len( points ) < 3: - print( - "\033[91mWarning:" - " Path segment has only {} points (not a polygon?)\033[0m".format( - len( points ) - ), file=sys.stderr - ) + logging.warning("Warning: Path segment has only {} points (not a polygon?)".format(len( points ))) #------------------------------------------------------------------------ @@ -280,17 +301,13 @@ def _find_insertion_point( self, hole, holes, other_insertions ): ):break else: - if self.verbose: - print( " Found insertion point: {}, {}".format( cp, hp ) ) + logging.debug( " Found insertion point: {}, {}".format( cp, hp ) ) # No other holes intersected, so this insertion point # is acceptable: return ( cp, hole.points_starting_on_index( hp ) ) - print( - "\033[91mCould not insert segment without overlapping other segments\033[0m", - file=sys.stderr - ) + logging.error("Could not insert segment without overlapping other segments") #------------------------------------------------------------------------ @@ -323,8 +340,7 @@ def inline( self, segments ): if len( segments ) < 1: return self.points - if self.verbose: - print( " Inlining {} segments...".format( len( segments ) ) ) + logging.debug( " Inlining {} segments...".format( len( segments ) ) ) all_segments = segments[ : ] + [ self ] insertions = [] @@ -460,25 +476,23 @@ def _prune_hidden( self, items = None ): continue if item.hidden : - if self.verbose: - print("Ignoring hidden SVG layer: {}".format( item.name ) ) + logging.warning("Ignoring hidden SVG layer: {}".format( item.name ) ) elif item.name is not "": self.svg.items.append( item ) if(item.items): self._prune_hidden( item.items ) - def __init__( self, file_name, module_name, module_value, ignore_hidden_layers, verbose_print ): + def __init__( self, file_name, module_name, module_value, ignore_hidden_layers ): self.file_name = file_name self.module_name = module_name self.module_value = module_value - self.verbose = verbose_print - print( "Parsing SVG..." ) - self.svg = svg.parse( file_name, verbose_print ) - if verbose_print: - print("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) + logging.getLogger("unfiltered").info( "Parsing SVG..." ) + + self.svg = svg.parse( file_name) + logging.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) if( ignore_hidden_layers ): self._prune_hidden() @@ -489,8 +503,6 @@ def __init__( self, file_name, module_name, module_value, ignore_hidden_layers, class Svg2ModExport( object ): - verbose = True - #------------------------------------------------------------------------ @staticmethod @@ -565,7 +577,6 @@ def __init__( use_mm = True, dpi = DEFAULT_DPI, pads = False, - verbose = True ): if use_mm: # 25.4 mm/in; @@ -583,7 +594,6 @@ def __init__( self.use_mm = use_mm self.dpi = dpi self.convert_pads = pads - self.verbose = verbose #------------------------------------------------------------------------ @@ -629,7 +639,7 @@ def _prune( self, items = None ): for name in self.layers.keys(): #if re.search( name, item.name, re.I ): if name == item.name and item.name is not "": - print( "Found SVG layer: {}".format( item.name ) ) + logging.getLogger("unfiltered").info( "Found SVG layer: {}".format( item.name ) ) self.imported.svg.items.append( item ) self.layers[ name ] = item @@ -655,7 +665,7 @@ def _write_items( self, items, layer, flip = False ): ): segments = [ - PolygonSegment( segment, verbose=self.verbose ) + PolygonSegment( segment ) for segment in item.segments( precision = self.precision ) @@ -678,23 +688,16 @@ def _write_items( self, items, layer, flip = False ): stroke_width ) - if self.verbose: - print( " Writing polygon with {} points".format( - len( points ) ) - ) + logging.info( " Writing polygon with {} points".format(len( points ) )) self._write_polygon( points, layer, fill, stroke, stroke_width ) - elif self.verbose: - print( "\033[91mSkipping {} with 0 points\033[0m".format( - item.__class__.__name__ - ), file=sys.stderr) + else: + logging.info( "Skipping {} with 0 points".format(item.__class__.__name__)) else: - print( "\033[91mUnsupported SVG element: {}\033[0m".format( - item.__class__.__name__ - ), file=sys.stderr) + logging.warning( "Unsupported SVG element: {}".format(item.__class__.__name__)) #------------------------------------------------------------------------ @@ -797,7 +800,7 @@ def write( self ): # Must come after pruning: translation = self._calculate_translation() - print( "Writing module file: {}".format( self.file_name ) ) + logging.getLogger("unfiltered").info( "Writing module file: {}".format( self.file_name ) ) self.output_file = open( self.file_name, 'w' ) self._write_library_intro() @@ -845,7 +848,6 @@ def __init__( precision = 20.0, use_mm = True, dpi = DEFAULT_DPI, - verbose = True, ): super( Svg2ModExportLegacy, self ).__init__( svg2mod_import, @@ -856,7 +858,6 @@ def __init__( use_mm, dpi, pads = False, - verbose = verbose ) self.include_reverse = True @@ -1040,7 +1041,6 @@ def __init__( precision = 20.0, dpi = DEFAULT_DPI, include_reverse = True, - verbose = True ): self.file_name = file_name use_mm = self._parse_output_file() @@ -1053,7 +1053,6 @@ def __init__( precision, use_mm, dpi, - verbose = verbose, ) @@ -1061,8 +1060,7 @@ def __init__( def _parse_output_file( self ): - if self.verbose: - print( "Parsing module file: {}".format( self.file_name ) ) + logging.info( "Parsing module file: {}".format( self.file_name ) ) module_file = open( self.file_name, 'r' ) lines = module_file.readlines() module_file.close() @@ -1142,8 +1140,7 @@ def _read_module( self, lines, index ): m = re.match( r'\$MODULE[\s]+([^\s]+)[\s]*', lines[ index ] ) module_name = m.group( 1 ) - if self.verbose: - print( " Reading module {}".format( module_name ) ) + logging.info( " Reading module {}".format( module_name ) ) index += 1 module_lines = [] @@ -1528,6 +1525,15 @@ def get_arguments(): default = False, ) + parser.add_argument( + '--debug', + dest = 'debug_print', + action = 'store_const', + const = True, + help = "Print debug level messages", + default = False, + ) + parser.add_argument( '-x', '--exclude-hidden', dest = 'ignore_hidden_layers', From 98043794d926917d7fd7c896f98c60015b7771ef Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 27 Jan 2021 19:02:47 -0700 Subject: [PATCH 064/151] Rework the logger so it can use stderr and stdout If the logger prints a warning or error it is printed to stderr and others levels are printed to stdout --- .travis.yml | 4 ++-- README.md | 12 ++++++------ setup.py | 2 +- svg2mod/coloredlogger.py | 22 ++++++++++++++++++++-- svg2mod/svg2mod.py | 18 ++++-------------- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index 48494c2..4fcdfbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,8 @@ jobs: language: python script: - pip3 install ./ - - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 -v - - svg2mod -i examples/svg2mod.svg -v + - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug + - svg2mod -i examples/svg2mod.svg --debug - svg2mod -i examples/svg2mod.svg --format legacy - svg2mod -i examples/svg2mod.svg - stage: deploy diff --git a/README.md b/README.md index e9f7731..8e61eca 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # svg2mod -[![Build Status](https://travis-ci.com/svg2mod/svg2mod.svg?branch=master)](https://travis-ci.com/svg2mod/svg2mod) [![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod)](https://github.com/svg2mod/svg2mod/commits/master) +[![Build Status](https://travis-ci.com/svg2mod/svg2mod.svg?branch=master)](https://travis-ci.com/svg2mod/svg2mod) +[![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod)](https://github.com/svg2mod/svg2mod/commits/master) [![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=black)](https://pypi.org/project/svg2mod/) - [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod)](https://pypi.org/project/svg2mod/) - [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version)](https://pypi.org/project/svg2mod/) @@ -27,9 +26,9 @@ Python 3 ## Usage ``` -usage: svg2mod [-h] -i FILENAME [-o FILENAME] [-c] [-pads] [-v] [-x] [-d DPI] - [-f FACTOR] [-p PRECISION] [--format FORMAT] [--name NAME] - [--units UNITS] [--value VALUE] +usage: svg2mod [-h] -i FILENAME [-o FILENAME] [-c] [-pads] [-v] [--debug] [-x] + [-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] + [--name NAME] [--units UNITS] [--value VALUE] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -43,6 +42,7 @@ optional arguments: -pads, --convert-pads Convert any artwork on Cu layers to pads -v, --verbose Print more verbose messages + --debug Print debug level messages -x, --exclude-hidden Do not export hidden layers -d DPI, --dpi DPI DPI of the SVG file (int) -f FACTOR, --factor FACTOR diff --git a/setup.py b/setup.py index 8e88bf8..dd202f2 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ install_requires=requirements, license="CC0-1.0", zip_safe=False, - keywords='svg2mod, KiCAD', + keywords='svg2mod, KiCAD, inkscape', classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index 7be942e..fa4d7d1 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -1,3 +1,7 @@ +# A simple modification to the formatter class in the python logger to allow +# ANSI color codes based on the logged message's level + +import sys import logging @@ -9,7 +13,7 @@ class Formatter(logging.Formatter): logging.DEBUG: "\033[90m", #Set dark gray/black logging.INFO: "" #Do nothing } - reset = "\033[0m" + reset = "\033[0m" # Reset the terminal back to default color/emphasis def __init__(self, fmt="%(message)s", datefmt=None, style="%"): super().__init__(fmt, datefmt, style) @@ -18,4 +22,18 @@ def format(self, record): self._style._fmt = Formatter.color[record.levelno] + fmt_org + Formatter.reset result = logging.Formatter.format(self, record) self._style._fmt = fmt_org - return result \ No newline at end of file + return result + +def split_logger(logger, formatter=Formatter(), breakpoint=logging.WARNING): + hdlrerr = logging.StreamHandler(sys.stderr) + hdlrerr.addFilter(lambda msg: breakpoint <= msg.levelno) + + hdlrout = logging.StreamHandler(sys.stdout) + hdlrout.addFilter(lambda msg: breakpoint > msg.levelno) + + hdlrerr.setFormatter(formatter) + hdlrout.setFormatter(formatter) + logger.addHandler(hdlrerr) + logger.addHandler(hdlrout) + + \ No newline at end of file diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 0251368..f41ae0c 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1,12 +1,11 @@ import argparse import datetime -import os -import re -import svg2mod.svg as svg import logging +import os, sys, re + +import svg2mod.svg as svg import svg2mod.coloredlogger as coloredlogger -import sys #---------------------------------------------------------------------------- @@ -22,16 +21,7 @@ def main(): # Setup root logger to use terminal colored outputs as well as stdout and stderr - error_handler = logging.StreamHandler(sys.stderr) - error_handler.setFormatter(coloredlogger.Formatter()) - error_handler.addFilter(lambda record: logging.WARNING <= record.levelno) # Send logs with level ERROR and above to stderr - - log_handler = logging.StreamHandler(sys.stdout) - log_handler.setFormatter(coloredlogger.Formatter()) - log_handler.addFilter(lambda record: logging.WARNING > record.levelno) # Send logs with level INFO and below to stdout - - logging.root.addHandler(error_handler) - logging.root.addHandler(log_handler) + coloredlogger.split_logger(logging.root) if args.verbose_print: logging.root.setLevel(logging.INFO) From 35509cd62c8cefada33e84d6dfd22a257aac4efc Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Thu, 11 Feb 2021 12:07:49 +0100 Subject: [PATCH 065/151] Fix conversion of files without viewBox In commit 664ee86 (Fix verbose flag and stroke width calulation) a new `viewport_scale` variable was introduced into the Svg class. However, the default value set in the constructor lacked a `self.` prefix, so this attribute would not be set by default. On files with a `viewBox` element, this attribute would be set later, but files without this would lack the attribute and break: File "svg2mod/svg2mod.py", line 538, in _get_fill_stroke scale = self.imported.svg.viewport_scale * float(self.dpi) / 25.4 AttributeError: 'Svg' object has no attribute 'viewport_scale' --- svg2mod/svg/svg/svg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 6a53db0..288d723 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -209,7 +209,7 @@ class Svg(Transformable): # tag = 'svg' def __init__(self, filename=None, verbose=True): - viewport_scale = 1 + self.viewport_scale = 1 Transformable.__init__(self, verbose=verbose) if filename: self.parse(filename) From ba810ec0255346840d10fe056e40f4dc66356065 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Fri, 12 Feb 2021 12:51:13 +0100 Subject: [PATCH 066/151] Add import math, needed for transform: rotate --- svg2mod/svg/svg/svg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 288d723..369afda 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -22,6 +22,7 @@ import os import copy import re +import math import xml.etree.ElementTree as etree import itertools import operator From 7faafb0fe8c228a2ecd90410ef138931f28a3342 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Fri, 12 Feb 2021 21:43:43 +0100 Subject: [PATCH 067/151] Remove scale/translate/rotate methods These were never called anywhere, all transformations would use a transformation matrix. Remove them for more clarity. It seems these methods were already unused in the very first commit that added these files. --- svg2mod/svg/svg/geometry.py | 24 ------------------------ svg2mod/svg/svg/svg.py | 24 ------------------------ 2 files changed, 48 deletions(-) diff --git a/svg2mod/svg/svg/geometry.py b/svg2mod/svg/svg/geometry.py index 7a4114f..bad1c3a 100644 --- a/svg2mod/svg/svg/geometry.py +++ b/svg2mod/svg/svg/geometry.py @@ -202,16 +202,6 @@ def transform(self, matrix): self.start = matrix * self.start self.end = matrix * self.end - def scale(self, ratio): - self.start *= ratio - self.end *= ratio - def translate(self, offset): - self.start += offset - self.end += offset - def rotate(self, angle): - self.start = self.start.rot(angle) - self.end = self.end.rot(angle) - class Bezier: '''Bezier curve class A Bezier curve is defined by its control points @@ -297,13 +287,6 @@ def _bezierN(self, t): def transform(self, matrix): self.pts = [matrix * x for x in self.pts] - def scale(self, ratio): - self.pts = [x * ratio for x in self.pts] - def translate(self, offset): - self.pts = [x + offset for x in self.pts] - def rotate(self, angle): - self.pts = [x.rot(angle) for x in self.pts] - class MoveTo: def __init__(self, dest): self.dest = dest @@ -314,13 +297,6 @@ def bbox(self): def transform(self, matrix): self.dest = matrix * self.dest - def scale(self, ratio): - self.dest *= ratio - def translate(self, offset): - self.dest += offset - def rotate(self, angle): - self.dest = self.dest.rot(angle) - def simplify_segment(segment, epsilon): '''Ramer-Douglas-Peucker algorithm''' diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 369afda..6c53521 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -189,21 +189,6 @@ def flatten(self): i += 1 return flat - def scale(self, ratio): - for x in self.items: - x.scale(ratio) - return self - - def translate(self, offset): - for x in self.items: - x.translate(offset) - return self - - def rotate(self, angle): - for x in self.items: - x.rotate(angle) - return self - class Svg(Transformable): '''SVG class: use parse to parse a file''' # class Svg handles the tag @@ -588,15 +573,6 @@ def transform(self, matrix=None): self.rx = matrix.xlength(self.rx) self.ry = matrix.ylength(self.ry) - def scale(self, ratio): - self.center *= ratio - self.rx *= ratio - self.ry *= ratio - def translate(self, offset): - self.center += offset - def rotate(self, angle): - self.center = self.center.rot(angle) - def P(self, t): '''Return a Point on the Ellipse for t in [0..1]''' x = self.center.x + self.rx * math.cos(2 * math.pi * t) From 9f3479dd084f8ccfb86e8c56bf7ed12fcbdfbb3a Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Fri, 12 Feb 2021 22:08:08 +0100 Subject: [PATCH 068/151] Fix nested transformations for Ellipse, Rect and Line The transform method for Ellipse and Rect would already combine their own transformation with any parent transformation, but would have the order reversed. Line only included its own transformation. Paths used the transform method from Transformable, which was already correct. This fixes #27. --- svg2mod/svg/svg/svg.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 6c53521..3e4f395 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -568,7 +568,7 @@ def transform(self, matrix=None): if matrix is None: matrix = self.matrix else: - matrix = self.matrix * matrix + matrix *= self.matrix self.center = matrix * self.center self.rx = matrix.xlength(self.rx) self.ry = matrix.ylength(self.ry) @@ -654,7 +654,7 @@ def transform(self, matrix=None): if matrix is None: matrix = self.matrix else: - matrix = self.matrix*matrix + matrix *= self.matrix self.P1 = matrix * self.P1 self.P2 = matrix * self.P2 @@ -710,6 +710,10 @@ def bbox(self): return (Point(xmin,ymin), Point(xmax,ymax)) def transform(self, matrix): + if matrix is None: + matrix = self.matrix + else: + matrix *= self.matrix self.P1 = matrix * self.P1 self.P2 = matrix * self.P2 self.segment = Segment(self.P1, self.P2) From 6123b69bc4a4ce656ac853f1a76e7a197a9dc38f Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Sat, 13 Feb 2021 09:48:37 +0100 Subject: [PATCH 069/151] Apply commandline scaling factor to stroke widths Previously, using the --factor option would scale all points, but keep stroke widths at the same size, which could make the stroke widths way too small or large, especially with big scaling. With this commit the Svg2ModExport.scale_factor is used to calculate the stroke width, which includes the commandline scaling factor. Since it also includes the dpi conversion and (when needed), the decimil conversion, those conversions can be removed for the stroke width (so this changes the result of _get_fill_stroke to be output units (mm or decimil), rather than always mm. This fixes a part of #25. --- svg2mod/svg2mod.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index e334608..c649e9d 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -534,8 +534,10 @@ def _get_fill_stroke( self, item ): elif name == "stroke-width": stroke_width = float( "".join(i for i in value if not i.isalpha()) ) - # units per pixel converted to mm - scale = self.imported.svg.viewport_scale * float(self.dpi) / 25.4 + # units per pixel converted to output units + # TODO: Include all transformations instead of just + # the top-level viewport_scale + scale = self.imported.svg.viewport_scale / self.scale_factor # remove unessesary presion to reduce floating point errors stroke_width = round(stroke_width/scale, 6) @@ -548,7 +550,7 @@ def _get_fill_stroke( self, item ): stroke_width = 0.0 elif stroke_width is None: # Give a default stroke width? - stroke_width = self._convert_decimil_to_mm( 1 ) + stroke_width = self._convert_decimil_to_mm( 1 ) if self.use_mm else 1 return fill, stroke, stroke_width @@ -673,11 +675,6 @@ def _write_items( self, items, layer, flip = False ): points = segments[ 0 ].points if len ( segments ) != 0: - if not self.use_mm: - stroke_width = self._convert_mm_to_decimil( - stroke_width - ) - if self.verbose: print( " Writing polygon with {} points".format( len( points ) ) From 14fb3f65d346c6668d6410eb4652063a6d98d0d0 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Sat, 13 Feb 2021 08:50:19 +0100 Subject: [PATCH 070/151] Remove unused variable The function call has side effects, so that must stay --- svg2mod/svg2mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index e334608..0a89a31 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -795,7 +795,7 @@ def write( self ): self._prune() # Must come after pruning: - translation = self._calculate_translation() + self._calculate_translation() print( "Writing module file: {}".format( self.file_name ) ) self.output_file = open( self.file_name, 'w' ) From 94f4f997b58f5fbbb942e24c1c7b2a579c0d7707 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Sat, 13 Feb 2021 09:01:17 +0100 Subject: [PATCH 071/151] Write the full svg2mod commandline to the output file Previously, just the input filename was documented in the output file, but it is also useful to document the options that were used (i.e. precision, scaling, etc). This makes it easier to later redo the conversion after changing the input file, or making another size of the same file, etc. The easiest way to document all relevant options is to just write down the full commandline used, which is what this commit does. This is put into the description field for pretty files and the comment header for legacy files, replacing the input filename that was already there. Note that the legacy updater outupt code is not changed, since when updating a legacy footprint file rather than creating it, the header is copied from the original file rather than writing a new one. This could cause the command in the header to not actually match the content of the file. However, because this problem already exists for the filename in the header, and the legacy format is probably not actively used much if at all, this seems acceptable to me. --- svg2mod/svg2mod.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 0a89a31..2757d04 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -7,6 +7,7 @@ import re import svg2mod.svg as svg import sys +import shlex #---------------------------------------------------------------------------- @@ -103,8 +104,11 @@ def main(): verbose = args.verbose_print, ) + args = [os.path.basename(sys.argv[0])] + sys.argv[1:] + cmdline = ' '.join(shlex.quote(x) for x in args) + # Export the footprint: - exported.write() + exported.write(cmdline) #---------------------------------------------------------------------------- @@ -790,7 +794,7 @@ def transform_point( self, point, flip = False ): #------------------------------------------------------------------------ - def write( self ): + def write( self, cmdline ): self._prune() @@ -800,7 +804,7 @@ def write( self ): print( "Writing module file: {}".format( self.file_name ) ) self.output_file = open( self.file_name, 'w' ) - self._write_library_intro() + self._write_library_intro(cmdline) self._write_modules() @@ -886,7 +890,7 @@ def _get_module_name( self, front = None ): #------------------------------------------------------------------------ - def _write_library_intro( self ): + def _write_library_intro( self, cmdline ): modules_list = self._get_module_name( front = True ) if self.include_reverse: @@ -904,13 +908,13 @@ def _write_library_intro( self ): {2} $EndINDEX # -# {3} +# Converted using: {3} # """.format( datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ), units, modules_list, - self.imported.file_name, + cmdline ) ) @@ -1167,7 +1171,7 @@ def _read_module( self, lines, index ): #------------------------------------------------------------------------ - def _write_library_intro( self ): + def _write_library_intro( self, cmdline ): # Write pre-index: self.output_file.writelines( self.pre_index ) @@ -1300,7 +1304,7 @@ def _get_module_name( self, front = None ): #------------------------------------------------------------------------ - def _write_library_intro( self ): + def _write_library_intro( self, cmdline ): self.output_file.write( """(module {0} (layer F.Cu) (tedit {1:8X}) (attr virtual) @@ -1311,7 +1315,7 @@ def _write_library_intro( self ): int( round( os.path.getctime( #1 self.imported.file_name ) ) ), - "Imported from {}".format( self.imported.file_name ), #2 + "Converted using: {}".format( cmdline ), #2 "svg2mod", #3 ) ) From 5f07359a8ef0629c07aa7de54298b22e93dbc046 Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Sat, 13 Feb 2021 09:21:27 +0100 Subject: [PATCH 072/151] Clarify meaning of --precision option --- svg2mod/svg2mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 2757d04..0f9cab6 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1564,7 +1564,7 @@ def get_arguments(): type = float, dest = 'precision', metavar = 'PRECISION', - help = "smoothness for approximating curves with line segments (float)", + help = "smoothness for approximating curves with line segments. Input is the approximate length for each line segment in SVG pixels (float)", default = 10.0, ) parser.add_argument( From 5bd81c5219ceb30ffd324ea0f7aa6243106f005c Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 15 Feb 2021 02:53:30 -0700 Subject: [PATCH 073/151] Added arc support for the svg parser --- README.md | 18 ++--- svg2mod/coloredlogger.py | 76 +++++++++--------- svg2mod/svg/svg/geometry.py | 13 ++-- svg2mod/svg/svg/svg.py | 149 ++++++++++++++++++++++++++++++++++-- 4 files changed, 197 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 8e61eca..8eab8e8 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version)](https://pypi.org/project/svg2mod/) -__[@mtl](https://github.com/mtl) is no longer active. [https://github.com/svg2mod/svg2mod](https://github.com/svg2mod/svg2mod) is now the maintained branch.__ - This is a small program to convert Inkscape SVG drawings to KiCad footprint module files. It uses [a fork of cjlano's python SVG parser and drawing module](https://github.com/svg2mod/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 @@ -17,12 +15,12 @@ Python 3 ## Installation -```pip3 install svg2mod``` +```pip install svg2mod``` ## Example -```svg2mod -i input.svg -p 1.0``` +```svg2mod -i input.svg``` ## Usage ``` @@ -60,18 +58,18 @@ optional arguments: ## SVG Files -svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. +svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. This is so it can associate inkscape layers with kicad layers * 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. - * Paths are fully supported Rect and Circles (Ellipse) are partially supported. + * Paths are fully supported Rect and Arcs are partially supported. * A path may have an outline and a fill. (Colors will be ignored.) * A path may have holes, defined by interior segments within the path (see included examples). * A path with a filled area inside a hole will not work properly. You must split these apart into two seperate paths. - * Transparent fills and strokes with be ignored. + * If the arc end points are outside of the ellipse created from the same info. Ie. it radii are too small it will not scale it to size properly. + * 100% Transparent fills and strokes with be ignored. * Rect supports rotations, but not corner radii. - * Ellipses do not support rotation. * 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 named to match the target in kicad. The supported layers are listed below. - * Other types of elements such as arc are not supported. + * Layers must be named to match the target in kicad. The supported layers are listed below. They will be ignored otherwise. + * If there is an issue parsing an inkscape object or stroke convert it to a path. * Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these elements into paths that will work. ### Layers diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index fa4d7d1..5c15744 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -1,39 +1,39 @@ -# A simple modification to the formatter class in the python logger to allow -# ANSI color codes based on the logged message's level - -import sys -import logging - - -class Formatter(logging.Formatter): - color = { - logging.CRITICAL: "\033[91m\033[7m", #Set red and swap background and foreground - logging.ERROR: "\033[91m", #Set red - logging.WARNING: "\033[93m", #Set yellow - logging.DEBUG: "\033[90m", #Set dark gray/black - logging.INFO: "" #Do nothing - } - reset = "\033[0m" # Reset the terminal back to default color/emphasis - def __init__(self, fmt="%(message)s", datefmt=None, style="%"): - super().__init__(fmt, datefmt, style) - - def format(self, record): - fmt_org = self._style._fmt - self._style._fmt = Formatter.color[record.levelno] + fmt_org + Formatter.reset - result = logging.Formatter.format(self, record) - self._style._fmt = fmt_org - return result - -def split_logger(logger, formatter=Formatter(), breakpoint=logging.WARNING): - hdlrerr = logging.StreamHandler(sys.stderr) - hdlrerr.addFilter(lambda msg: breakpoint <= msg.levelno) - - hdlrout = logging.StreamHandler(sys.stdout) - hdlrout.addFilter(lambda msg: breakpoint > msg.levelno) - - hdlrerr.setFormatter(formatter) - hdlrout.setFormatter(formatter) - logger.addHandler(hdlrerr) - logger.addHandler(hdlrout) - +# A simple modification to the formatter class in the python logger to allow +# ANSI color codes based on the logged message's level + +import sys +import logging + + +class Formatter(logging.Formatter): + color = { + logging.CRITICAL: "\033[91m\033[7m", #Set red and swap background and foreground + logging.ERROR: "\033[91m", #Set red + logging.WARNING: "\033[93m", #Set yellow + logging.DEBUG: "\033[90m", #Set dark gray/black + logging.INFO: "" #Do nothing + } + reset = "\033[0m" # Reset the terminal back to default color/emphasis + def __init__(self, fmt="%(message)s", datefmt=None, style="%"): + super().__init__(fmt, datefmt, style) + + def format(self, record): + fmt_org = self._style._fmt + self._style._fmt = Formatter.color[record.levelno] + fmt_org + Formatter.reset + result = logging.Formatter.format(self, record) + self._style._fmt = fmt_org + return result + +def split_logger(logger, formatter=Formatter(), breakpoint=logging.WARNING): + hdlrerr = logging.StreamHandler(sys.stderr) + hdlrerr.addFilter(lambda msg: breakpoint <= msg.levelno) + + hdlrout = logging.StreamHandler(sys.stdout) + hdlrout.addFilter(lambda msg: breakpoint > msg.levelno) + + hdlrerr.setFormatter(formatter) + hdlrout.setFormatter(formatter) + logger.addHandler(hdlrerr) + logger.addHandler(hdlrout) + \ No newline at end of file diff --git a/svg2mod/svg/svg/geometry.py b/svg2mod/svg/svg/geometry.py index 7a4114f..1593f7e 100644 --- a/svg2mod/svg/svg/geometry.py +++ b/svg2mod/svg/svg/geometry.py @@ -104,7 +104,7 @@ def __repr__(self): return '(' + format(self.x,'.3f') + ',' + format( self.y,'.3f') + ')' def __str__(self): - return self.__repr__(); + return self.__repr__() def coord(self): '''Return the point tuple (x,y)''' @@ -114,14 +114,17 @@ def length(self): '''Vector length, Pythagoras theorem''' return math.sqrt(self.x ** 2 + self.y ** 2) - def rot(self, angle): + def rot(self, angle, x=0, y=0): '''Rotate vector [Origin,self] ''' if not isinstance(angle, Angle): try: angle = Angle(angle) except: return NotImplemented - x = self.x * angle.cos - self.y * angle.sin - y = self.x * angle.sin + self.y * angle.cos - return Point(x,y) + if angle.angle % (2 * math.pi) == 0: + return Point(self.x,self.y) + + new_x = ((self.x-x) * angle.cos) - ((self.y-y) * angle.sin) + x + new_y = ((self.x-x) * angle.sin) + ((self.y-y) * angle.cos) + y + return Point(new_x,new_y) class Angle: diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 9ad965e..a43d4c9 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -510,9 +510,10 @@ def parse(self, pathstr): else: x = pathlst.pop() y = pathlst.pop() # TODO - logging.warning("Unsupported ARC: , ".join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) -# self.items.append( -# Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) + self.items.append( + Arc(current_pt, rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) + #Arc(current_pt, rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y)) + current_pt = Point(x, y) else: pathlst.pop() @@ -567,6 +568,7 @@ def __repr__(self): def bbox(self): '''Bounding box''' + #TODO change bounding box dependent on rotation pmin = self.center - Point(self.rx, self.ry) pmax = self.center + Point(self.rx, self.ry) return (pmin, pmax) @@ -591,13 +593,12 @@ def rotate(self, angle): def P(self, t): '''Return a Point on the Ellipse for t in [0..1]''' + #TODO change point cords if rotaion x = self.center.x + self.rx * math.cos(2 * math.pi * t) y = self.center.y + self.ry * math.sin(2 * math.pi * t) return Point(x,y) def segments(self, precision=0): - if self.rotation % 180 != 0: - logging.warning("Unsupported rotation for {} primitive".format(self.__class__.__name__)) if max(self.rx, self.ry) < precision: return [[self.center]] @@ -611,12 +612,148 @@ def segments(self, precision=0): p.append((t, self.P(t))) p.sort(key=operator.itemgetter(0)) - ret = [x for t,x in p] + ret = [x.rot(math.radians(self.rotation), x=self.center.x, y=self.center.y) for t,x in p] return [ret] def simplify(self, precision): return self +# An arc is an ellipse with a begining and an end point instead of an entire circumference +class Arc(Ellipse): + '''SVG ''' + # class Ellipse handles the tag + tag = 'ellipse' + + def __init__(self, start_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt): + try: + self.rx = float(rx) + self.ry = float(ry) + self.rotation = float(xrot) + self.large_arc_flag = large_arc_flag=='1' + self.sweep_flag = sweep_flag=='1' + except: + pass + self.end_pts = [start_pt, end_pt] + + self.calcuate_center() + Ellipse.__init__(self, None) + #logging.debug(type(self.center)) + + def __repr__(self): + return '' + + def calcuate_center(self): + angle = Angle(math.radians(self.rotation)) + pts = self.end_pts + + # set some variables that are used often to decrease size of final equations + cs2 = 2*angle.cos*angle.sin*(math.pow(self.ry, 2) - math.pow(self.rx, 2)) + rs = (math.pow(self.ry*angle.sin, 2) + math.pow(self.rx*angle.cos, 2)) + rc = (math.pow(self.ry*angle.cos, 2) + math.pow(self.rx*angle.sin, 2)) + + + # Create a line that passes through both intersection points + y = -pts[0].x*(cs2) + pts[1].x*cs2 - 2*pts[0].y*rs + 2*pts[1].y*rs + # Round to prevent floating point errors + y = round(y, 10) + # A vertical line will break the program so we cannot calculate with these equations + if y != 0: + # Finish calculating the line + m = ( -2*pts[0].x*rc + 2*pts[1].x*rc - pts[0].y*cs2 + pts[1].y*cs2 ) / -y + b = ( math.pow(pts[0].x,2)*rc - math.pow(pts[1].x,2)*rc + pts[0].x*pts[0].y*cs2 - pts[1].x*pts[1].y*cs2 + math.pow(pts[0].y,2)*(rs) - math.pow(pts[1].y,2)*rs ) / -y + + # Now that we have a line we can setup a quadratic equation to solve for all intersection points + qa = rc + m*cs2 + math.pow(m,2)*rs + qb = -2*pts[0].x*rc + b*cs2 - pts[0].y*cs2 - m*pts[0].x*cs2 + 2*m*b*rs - 2*pts[0].y*m*rs + qc = math.pow(pts[0].x,2)*rc - b*pts[0].x*cs2 + pts[0].x*pts[0].y*cs2 + math.pow(b,2)*rs - 2*b*pts[0].y*rs + math.pow(pts[0].y,2)*rs - math.pow(self.rx*self.ry, 2) + + else: + # When the slope is vertical we need to calculate with x instead of y + x = (pts[0].x+pts[1].x)/2 + m=0 + b=x + + # The quadratic formula but solving for y instead of x and only when the slope is vertical + qa = rs + qb = x*cs2 - pts[0].x*cs2 - 2*pts[0].y*rs + qc = math.pow(x,2)*rc - 2*x*pts[0].x*rc + math.pow(pts[0].x,2)*rc - x*pts[0].y*cs2 + pts[0].x*pts[0].y*cs2 + math.pow(pts[0].y,2)*rs - math.pow(self.rx*self.ry, 2) + + # This is the value to see how many real solutions the quadratic equation has. + # if root is negative then there are only imaginary solutions or no real solutions + # if the root is 0 then there is one solution + # otherwise there are two solutions + root = math.pow(qb, 2) - 4*qa*qc + + # If there are no roots then we need to scale the arc to fit the points + if root < 0: + logging.warning("Invalid arc: {}, {} {} {} {} {}".format(self.rx, self.ry, self.rotation, self.large_arc_flag, self.sweep_flag, self.end_pts[1])) + point = Point((pts[0].x + pts[1].x)/2,(pts[0].y + pts[1].y)/2) + + # finish solving the quadratic equation and find the corresponding points on the intersection line + elif root == 0: + xroot = (-qb+math.sqrt(root))/(2*qa) + point = Point(xroot, xroot*m + b) + # Using the provided large_arc and sweep flags to choose the correct root + else: + xroots = [(-qb+math.sqrt(root))/(2*qa), (-qb-math.sqrt(root))/(2*qa)] + points = [Point(xroots[0], xroots[0]*m + b), Point(xroots[1], xroots[1]*m + b)] + # Calculate the angle of the begining point to the end point + + # If counterclockwise the two angles are the angle is within 180 degrees of each other: + # and no flags are set use the first center + # and the sweep flag is set use the second + # the large arc flag is set invert the previous selection + + angles = [] + for pt in pts: + pt = Point(pt.x-points[0].x, pt.y-points[0].y) + pt.rot(math.radians(-self.rotation)) + pt = Point(pt.x/self.rx, pt.y/self.ry) + angles.append(math.atan2(pt.y,pt.x)) + target = 0 + if self.sweep_flag: + target = (angles[0] - angles[1]) > 0 or (angles[0] - angles[1]) < math.pi + else: + target = (angles[0] - angles[1]) < 0 or (angles[0] - angles[1]) > math.pi + + point = points[target if not self.large_arc_flag else not target] + + + # Swap the x and y results from when the intersection line is vertical because we solved for y instead of x + # Also remove any insignificant floating point errors + if y == 0: + point = Point(round(point.y, 10), round(point.x, 10)) + else: + point = Point(round(point.x, 10), round(point.y, 10)) + self.center = point + + # Calculate start and end angle of the unrotated arc + self.angles = [] + for pt in self.end_pts: + pt = Point(pt.x-self.center.x, pt.y-self.center.y) + pt.rot(math.radians(-self.rotation)) + pt = Point(pt.x/self.rx, pt.y/self.ry) + self.angles.append(math.atan2(pt.y,pt.x)) + if not self.sweep_flag and self.angles[0] < self.angles[1]: + self.angles[0] += 2*math.pi + elif self.sweep_flag and self.angles[1] < self.angles[0]: + self.angles[1] += 2*math.pi + + + def segments(self, precision=0): + if max(self.rx, self.ry) < precision: + return self.end_pts + return Ellipse.segments(self, precision)[0] + + def P(self, t): + '''Return a Point on the Arc for t in [0..1]''' + #TODO change point cords if rotaion + x = self.center.x + self.rx * math.cos(((self.angles[1] - self.angles[0]) * t) + self.angles[0]) + y = self.center.y + self.ry * math.sin(((self.angles[1] - self.angles[0]) * t) + self.angles[0]) + return Point(x,y) + + + # A circle is a special type of ellipse where rx = ry = radius class Circle(Ellipse): '''SVG ''' From 8619c751724a7c45e7442eade30136aaaa5a0619 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 17 Feb 2021 20:57:08 -0700 Subject: [PATCH 074/151] Create FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..cd0b774 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [Sodium-Hydrogen] From 399593aa8e06eb570c58bb12f1ea900d02d644ff Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 22 Feb 2021 14:45:22 -0700 Subject: [PATCH 075/151] Added support for inkscape arc object Also fixed bug with choosing arc center not always working. --- svg2mod/svg/svg/svg.py | 59 +++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index a43d4c9..418534b 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -300,7 +300,7 @@ def append(self, element): for elt in element: elt_class = svgClass.get(elt.tag, None) if elt_class is None: - logging.warning('No handler for element %s' % elt.tag) + logging.debug('No handler for element %s' % elt.tag) continue # instanciate elt associated class (e.g. : item = Path(elt) item = elt_class(elt) @@ -509,11 +509,11 @@ def parse(self, pathstr): if len(flags) > 1: x = flags[1:] else: x = pathlst.pop() y = pathlst.pop() - # TODO + end_pt = Point(x, y) + if not absolute: end_pt += current_pt self.items.append( - Arc(current_pt, rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) - #Arc(current_pt, rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y)) - current_pt = Point(x, y) + Arc(current_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt)) + current_pt = end_pt else: pathlst.pop() @@ -553,6 +553,7 @@ class Ellipse(Transformable): '''SVG ''' # class Ellipse handles the tag tag = 'ellipse' + arc = False def __init__(self, elt=None): Transformable.__init__(self, elt) @@ -562,6 +563,10 @@ def __init__(self, elt=None): self.rx = self.length(elt.get('rx')) self.ry = self.length(elt.get('ry')) self.style = elt.get('style') + if elt.get('d') is not None: + self.arc = True + self.path = Path(elt) + self.path_str = elt.get('d') def __repr__(self): return '' @@ -599,6 +604,9 @@ def P(self, t): return Point(x,y) def segments(self, precision=0): + if self.arc: + segs = self.path.segments(precision) + return segs if max(self.rx, self.ry) < precision: return [[self.center]] @@ -625,6 +633,7 @@ class Arc(Ellipse): tag = 'ellipse' def __init__(self, start_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt): + Ellipse.__init__(self, None) try: self.rx = float(rx) self.ry = float(ry) @@ -634,10 +643,9 @@ def __init__(self, start_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt): except: pass self.end_pts = [start_pt, end_pt] + self.angles = [] self.calcuate_center() - Ellipse.__init__(self, None) - #logging.debug(type(self.center)) def __repr__(self): return '' @@ -686,8 +694,19 @@ def calcuate_center(self): # If there are no roots then we need to scale the arc to fit the points if root < 0: - logging.warning("Invalid arc: {}, {} {} {} {} {}".format(self.rx, self.ry, self.rotation, self.large_arc_flag, self.sweep_flag, self.end_pts[1])) + # Center point point = Point((pts[0].x + pts[1].x)/2,(pts[0].y + pts[1].y)/2) + # Angle between center and one of the end points adjusted to remove rotation from original data + ptAng = math.atan2(self.end_pts[0].y-point.y, self.end_pts[0].x-point.x) - angle.angle + # Adjust the angle to compensate for ellipse irregularity + ptAng = math.atan((self.rx/self.ry) * math.tan(ptAng)) + # Calculate scaling factor between provided ellipse and actual end points + radius = math.sqrt(math.pow(self.rx * math.cos(ptAng),2) + math.pow(self.ry * math.sin(ptAng),2)) + dist = math.sqrt( math.pow(self.end_pts[0].x-point.x, 2) + math.pow(self.end_pts[0].y-point.y, 2)) + factor = dist/radius + self.rx *= factor + self.ry *= factor + # finish solving the quadratic equation and find the corresponding points on the intersection line elif root == 0: @@ -704,19 +723,21 @@ def calcuate_center(self): # and the sweep flag is set use the second # the large arc flag is set invert the previous selection + # Don't save the angles because they are calculated from the first possible center. + # This may change so we'll just recalculate the angles later on angles = [] for pt in pts: pt = Point(pt.x-points[0].x, pt.y-points[0].y) pt.rot(math.radians(-self.rotation)) pt = Point(pt.x/self.rx, pt.y/self.ry) - angles.append(math.atan2(pt.y,pt.x)) + angles.append(math.atan2(pt.y,pt.x)%(math.pi*2)) target = 0 if self.sweep_flag: - target = (angles[0] - angles[1]) > 0 or (angles[0] - angles[1]) < math.pi + target = 0 if (angles[0] - angles[1]) < 0 or (angles[0] - angles[1]) > math.pi else 1 else: - target = (angles[0] - angles[1]) < 0 or (angles[0] - angles[1]) > math.pi + target = 1 if (angles[0] - angles[1]) < 0 or (angles[0] - angles[1]) > math.pi else 0 - point = points[target if not self.large_arc_flag else not target] + point = points[target if not self.large_arc_flag else target ^ 1 ] # Swap the x and y results from when the intersection line is vertical because we solved for y instead of x @@ -728,12 +749,14 @@ def calcuate_center(self): self.center = point # Calculate start and end angle of the unrotated arc - self.angles = [] - for pt in self.end_pts: - pt = Point(pt.x-self.center.x, pt.y-self.center.y) - pt.rot(math.radians(-self.rotation)) - pt = Point(pt.x/self.rx, pt.y/self.ry) - self.angles.append(math.atan2(pt.y,pt.x)) + if len(self.angles) < 2: + self.angles = [] + for pt in self.end_pts: + pt = Point(pt.x-self.center.x, pt.y-self.center.y) + pt = pt.rot(math.radians(-self.rotation)) + pt = Point(pt.x/self.rx, pt.y/self.ry) + self.angles.append(math.atan2(pt.y,pt.x)) + if not self.sweep_flag and self.angles[0] < self.angles[1]: self.angles[0] += 2*math.pi elif self.sweep_flag and self.angles[1] < self.angles[0]: From 63247e19edb86722410b6cee10ea5514572ff2ce Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 22 Feb 2021 23:32:42 -0700 Subject: [PATCH 076/151] Clean up import statements Adjust comments to be more descriptive and update svg parser copyright notice --- svg2mod/coloredlogger.py | 2 ++ svg2mod/svg/README.md | 6 +++--- svg2mod/svg/svg/geometry.py | 5 ++--- svg2mod/svg/svg/svg.py | 12 ++++-------- svg2mod/svg2mod.py | 7 ++----- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index 5c15744..f10b306 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -24,6 +24,8 @@ def format(self, record): self._style._fmt = fmt_org return result +# This will split logging messegaes at breakpoint. Anything higher will be sent +# to sys.stderr and everything else to sys.stdout def split_logger(logger, formatter=Formatter(), breakpoint=logging.WARNING): hdlrerr = logging.StreamHandler(sys.stderr) hdlrerr.addFilter(lambda msg: breakpoint <= msg.levelno) diff --git a/svg2mod/svg/README.md b/svg2mod/svg/README.md index ae4ce0d..179a03d 100644 --- a/svg2mod/svg/README.md +++ b/svg2mod/svg/README.md @@ -1,4 +1,4 @@ -SVG parser library +# SVG parser library ================== This is a SVG parser library written in Python and is currently only developed to support @@ -7,13 +7,13 @@ This is a SVG parser library written in Python and is currently only developed t Capabilities: - Parse SVG XML - - apply any transformation (svg transform) + - Apply any transformation (svg transform) - Explode SVG Path into basic elements (Line, Bezier, ...) - Interpolate SVG Path as a series of segments - Able to simplify segments given a precision using Ramer-Douglas-Peucker algorithm Not (yet) supported: - - SVG Path Arc ('A') - Non-linear transformation drawing (SkewX, ...) + - Text elements (\) License: GPLv2+ diff --git a/svg2mod/svg/svg/geometry.py b/svg2mod/svg/svg/geometry.py index 1593f7e..9db9cd3 100644 --- a/svg2mod/svg/svg/geometry.py +++ b/svg2mod/svg/svg/geometry.py @@ -1,4 +1,5 @@ # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > +# Copyright (C) 2021 -- svg2mod developers < github.com / svg2mod > # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,9 +20,7 @@ related to SVG parsing. It can be reused outside the scope of SVG. ''' -import math -import numbers -import operator +import math, numbers, operator class Point: def __init__(self, x=None, y=None): diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 418534b..c5c26dc 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -1,6 +1,7 @@ # SVG parser in Python # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > +# Copyright (C) 2021 -- svg2mod developers < github.com / svg2mod > # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,15 +19,10 @@ from __future__ import absolute_import import traceback -import sys -import os -import copy -import re +import sys, os, copy, re import xml.etree.ElementTree as etree -import itertools -import operator -import json -import logging +import itertools, operator +import json, logging from .geometry import * diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index f443623..9fa73fc 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1,13 +1,10 @@ -import argparse -import datetime +import argparse, datetime +import shlex, os, sys, re import logging -import os, sys, re import svg2mod.svg as svg import svg2mod.coloredlogger as coloredlogger -import sys -import shlex #---------------------------------------------------------------------------- From 01256c35c841eefc1058dca1aeb5a1778496e682 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 22 Feb 2021 23:50:55 -0700 Subject: [PATCH 077/151] Update svg2mod/svg/README.md --- svg2mod/svg/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/svg2mod/svg/README.md b/svg2mod/svg/README.md index 179a03d..1e16510 100644 --- a/svg2mod/svg/README.md +++ b/svg2mod/svg/README.md @@ -1,5 +1,6 @@ # SVG parser library -================== + +----------------------- This is a SVG parser library written in Python and is currently only developed to support [svg2mod](https://github.com/svg2mod/svg2mod). From a3406168ed879de43d8665677f5b8a1001b6dc3c Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 22 Feb 2021 23:55:02 -0700 Subject: [PATCH 078/151] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8eab8e8..366bb74 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,13 @@ optional arguments: svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. This is so it can associate inkscape layers with kicad layers * 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. - * Paths are fully supported Rect and Arcs are partially supported. + * Paths are fully supported Rect are partially supported. * A path may have an outline and a fill. (Colors will be ignored.) * A path may have holes, defined by interior segments within the path (see included examples). * A path with a filled area inside a hole will not work properly. You must split these apart into two seperate paths. - * If the arc end points are outside of the ellipse created from the same info. Ie. it radii are too small it will not scale it to size properly. * 100% Transparent fills and strokes with be ignored. * Rect supports rotations, but not corner radii. + * Text Elements are not supported * 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 named to match the target in kicad. The supported layers are listed below. They will be ignored otherwise. * If there is an issue parsing an inkscape object or stroke convert it to a path. From 399ba79d702f549b72e46023f3a49ebbb4bc5894 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Tue, 9 Mar 2021 14:06:26 -0700 Subject: [PATCH 079/151] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4fcdfbd..4181e6e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,5 +21,5 @@ jobs: secure: "VtbTcUsotXbHcGyVXT0xrErRHt3j104GX0LzuvKEc/uRLGr+Eo3CUHguFSanQ7p2LH2k0DTEmrzMfLVHltXLPmAqvUL3MrM0oUSCf4bkPqcLWiGhHYYtWs4XiYs3aS7esM0SKOyFBHZ1z/Y9JoKIywZgiMD/CI/XqMI5IHfDmy9XAxlqgTN0COp2NWP9SGi2VdJ0sebm7jVGt8kp99yq/2F+VDSADutoVhx68+XMdgd/JMm9Q8ouSra0ni9jpfyN8JJ59ucj8i2LUhz14+zQGcMAYm0QbIhKuibbHsDImLp1vxubAwbvaeA6K5jerZKme8wLAvilLm17bly5RivHER21IW53hccVTRxIzkt3nhJfaU0XBbJ8nxJJJ7F6Rrpzn6Tpcvx4WgODA/0nQ1DRUoGXBiLPO987q7SaYJdP4Ay4dZSTBXt1Rv039IZvTHs0M0MORXB4lSBQ0S0oAJzOA74+jptTW+z+rktXXL8yPBxSIbq7wSPOmrl56gxhojveiXVh6t51+HT16bV4gFRB3SYacDFxSN5ZAGI4ziaBEFYm/PTP5HHciat7qMP7I/8QhV1pkl7IXlENC1I0miYTQN77jzE9Z8RU2rEtYzZBNH5M5agr2AmFrTRu2HD+fINcAyMHvDyrYCnonss7oWjzoJb8PqpkDpSyNsrYGFyi1VU=" distributions: "sdist bdist_wheel" on: - branch: master + branch: main tags: true From 103590e0dd69cab10be879af2ece970177cc986c Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 12 Mar 2021 23:09:21 -0700 Subject: [PATCH 080/151] Added text support and better path inlining Basic support for text elements has been added as well as support to breakup distinct elements and nested elements. I've also implemented an expiremental path inlining algorithm. --- .gitignore | 1 + svg2mod/svg/svg/svg.py | 305 ++++++++++++++++++++++++++++++++++- svg2mod/svg2mod.py | 353 +++++++++++++++++++++++++++++++---------- 3 files changed, 574 insertions(+), 85 deletions(-) diff --git a/.gitignore b/.gitignore index 2281fcf..5becbb9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist svg2mod.egg-info *.kicad_mod *.mod +__pycache__ \ No newline at end of file diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index de3b880..b7e4a38 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -18,12 +18,20 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from __future__ import absolute_import +import xml.etree.ElementTree as etree import traceback, math import sys, os, copy, re -import xml.etree.ElementTree as etree import itertools, operator +import platform import json, logging from .geometry import * +from fontTools.ttLib import ttFont +from fontTools.pens.recordingPen import RecordingPen +from fontTools.pens.basePen import decomposeQuadraticSegment +from fontTools.misc import loggingTools + +# Make fontTools more quiet +loggingTools.configLogger(level=logging.INFO) svg_ns = '{http://www.w3.org/2000/svg}' @@ -54,6 +62,9 @@ def __init__(self, elt=None): self.id = hex(id(self)) # Unit transformation matrix on init self.matrix = Matrix() + self.scalex = 1 + self.scaley = 1 + self.style = "" self.rotation = 0 self.viewport = Point(800, 600) # default viewport is 800x600 if elt is not None: @@ -107,6 +118,8 @@ def getTransformations(self, elt): sx = arg[0] if len(arg) == 1: sy = sx else: sy = arg[1] + self.scalex *= sx + self.scaley *= sy self.matrix *= Matrix([sx, 0, 0, sy, 0, 0]) if op == 'rotate': @@ -588,9 +601,9 @@ def segments(self, precision=0): while d > precision: for (t1,p1),(t2,p2) in zip(p[:-1],p[1:]): t = t1 + (t2 - t1)/2. - d = Segment(p1, p2).pdistance(self.P(t)) p.append((t, self.P(t))) p.sort(key=operator.itemgetter(0)) + d = Segment(p[0][1],p[1][1]).length() ret = [x.rot(math.radians(self.rotation), x=self.center.x, y=self.center.y) for t,x in p] return [ret] @@ -624,9 +637,9 @@ def __repr__(self): def calcuate_center(self): angle = Angle(math.radians(self.rotation)) - pts = self.end_pts # set some variables that are used often to decrease size of final equations + pts = self.end_pts cs2 = 2*angle.cos*angle.sin*(math.pow(self.ry, 2) - math.pow(self.rx, 2)) rs = (math.pow(self.ry*angle.sin, 2) + math.pow(self.rx*angle.cos, 2)) rc = (math.pow(self.ry*angle.cos, 2) + math.pow(self.rx*angle.sin, 2)) @@ -867,6 +880,292 @@ def segments(self, precision=0): def simplify(self, precision): return self.segments(precision) +class Text(Transformable): + '''SVG ''' + # class Text handles the tag + tag = 'text' + + default_font = None + _system_fonts = {} + _os_font_paths = { + "Darwin": ["/Library/Fonts", "~/Library/Fonts"], + "Linux": ["/usr/share/fonts","/usr/local/share/fonts","~/.local/share/fonts"], + "Windows": ["C:/Windows/Fonts", "~/AppData/Local/Microsoft/Windows/Fonts"] + } + + def __init__(self, elt=None, parent=None): + Transformable.__init__(self, elt) + + self.bbox_points = [Point(0,0), Point(0,0)] + + if elt is not None: + self.style = elt.get('style') + self.parse(elt, parent) + if parent is None: + self.convert_to_path(auto_transform=False) + else: + self.origin = Point(0,0) + self.font_family = Text.default_font + self.size = 12 + self.bold = "normal" + self.italic = "normal" + if self.font_family: + self.font_file = self.find_font_file() + self.text = [] + + def set_font(self, font=None, bold=None, italic=None, size=None): + font = font if font else self.font_family + bold = bold if bold else (self.bold.lower() != "normal") + italic = italic if italic else (self.italic.lower() != "normal") + size = size if size else self.size + if type(size) is str: + size = float(size.strip("px")) + + self.font_family = font + self.size = size + self.bold = "normal" if not bold else "bold" + self.italic = "normal" if not italic else "italic" + self.font_file = self.find_font_file() + + + def add_text(self, text, origin=Point(0,0)): + if origin == self.origin: + self.text.append((text, self)) + else: + new_line = Text() + new_line.set_font( + font=self.font_family, + bold=(self.bold != "normal"), + italic=(self.italic != "normal"), + size=self.size + ) + + new_line.origin = origin + self.text.append((text, new_line)) + + + def parse(self, elt, parent): + x = elt.get('x') + y = elt.get('y') + + # It seems that any values in style that override these values take precidence + self.font_configs = { + "font-family": elt.get('font-family'), + "font-size": elt.get('font-size'), + "font-weight": elt.get('font-weight'), + "font-style": elt.get('font-style'), + } + if self.style is not None: + for style in self.style.split(";"): + nv = style.split(":") + name = nv[ 0 ].strip() + value = nv[ 1 ].strip() + if list(self.font_configs.keys()).count(name) != 0: + self.font_configs[name] = value + + if type(self.font_configs["font-size"]) is str: + float(self.font_configs["font-size"].strip("px")) + + for config in self.font_configs: + if self.font_configs[config] is None and parent is not None: + self.font_configs[config] = parent.font_configs[config] + + self.font_family = self.font_configs["font-family"] + self.size = self.font_configs["font-size"] + self.bold = self.font_configs["font-weight"] + self.italic = self.font_configs["font-style"] + + self.font_file = self.find_font_file() + + if parent is not None: + x = parent.origin.x if x is None else float(x) + y = parent.origin.y if y is None else float(y) + x = 0 if x is None else float(x) + y = 0 if y is None else float(y) + self.origin = Point(x,y) + + self.text = [] if elt.text is None else [(elt.text, self)] + for child in list(elt): + Text(child, self) + if parent is not None: + parent.text.extend(self.text) + if elt.tail is not None: + parent.text.append((elt.tail, parent)) + + del(self.font_configs) + + + def find_font_file(self): + if self.font_family is None: + if Text.default_font is None: + logging.error("Unable to find font because no font was specified.") + return None + self.font_family = Text.default_font + fonts = [fnt.strip().strip("'") for fnt in self.font_family.split(",")] + if Text.default_font is not None: fonts.append(Text.default_font) + + font_files = None + target_font = None + for fnt in fonts: + if Text.load_system_fonts().get(fnt) is not None: + target_font = fnt + font_files = Text.load_system_fonts().get(fnt) + break + if font_files is None: + # We are unable to find a font and since there is no default font stop building font data + logging.error("Unable to find font(s) \"{}\"{}".format( + self.font_family, + " and no default font specified" if Text.default_font is None else f" or default font \"{Text.default_font}\"" + )) + self.paths = [] + return + + bold = self.bold is not None and self.bold.lower() != "normal" + italic = self.italic is not None and self.italic.lower() != "normal" + + reg = ["Regular", "Book"] + bol = ["Bold", "Demibold"] + ita = ["Italic", "Oblique"] + + search = reg + if bold and not italic: + search = bol + elif italic and not bold: + search = ita + elif italic and bold: + search = [f"{b} {i}" if n == 0 else f"{i} {b}" for b in bol for i in ita for n in range(2)] + tar_font = list(filter(None, [font_files.get(style) for style in search])) + if len(tar_font) == 0 and len(font_files.keys()) == 1: + tar_font = [font_files[list(font_files.keys())[0]]] + logging.warning("Font \"{}\" does not natively support style \"{}\" using \"{}\" instead".format( + target_font, search[0], list(font_files.keys())[0])) + elif len(tar_font) == 0 and italic and bold: + orig_search = search[0] + search = [] + search.extend(ita) + search.extend(bol) + search.extend(reg) + search.extend(list(font_files.keys())) + for style in search: + if font_files.get(style) is not None: + tar_font = [font_files[style]] + logging.warning("Font \"{}\" does not natively support style \"{}\" using \"{}\" instead".format( + target_font, orig_search, style)) + break + return tar_font[0] + + + def convert_to_path(self, auto_transform=True): + self.paths = [] + prev_origin = self.text[0][1].origin + + offset = Point(prev_origin.x, prev_origin.y) + for index, (text, attrib) in enumerate(self.text): + + if attrib.font_file is None or attrib.font_family is None: + continue + size = attrib.size + ttf = ttFont.TTFont(attrib.font_file) + offset.y = attrib.origin.y + ttf["head"].unitsPerEm + scale = size/offset.y + + if prev_origin != attrib.origin: + prev_origin = attrib.origin + offset.x = attrib.origin.x + + path = [] + for char in text: + + pathbuf = "" + pen = RecordingPen() + try: glf = ttf.getGlyphSet()[ttf.getBestCmap()[ord(char)]] + except KeyError: + logging.warning(f"Unsuported character in element \"{char}\"") + #txt = txt.replace(char, "") + continue + + glf.draw(pen) + for command in pen.value: + pts = list(command[1]) + for ptInd in range(len(pts)): + pts[ptInd] = (pts[ptInd][0], offset.y - pts[ptInd][1]) + if command[0] == "moveTo" or command[0] == "lineTo": + pathbuf += command[0][0].upper() + f" {pts[0][0]},{pts[0][1]} " + elif command[0] == "qCurveTo": + pts = decomposeQuadraticSegment(command[1]) + for pt in pts: + pathbuf += "Q {},{} {},{} ".format( + pt[0][0], offset.y - pt[0][1], + pt[1][0], offset.y - pt[1][1] + ) + elif command[0] == "closePath": + pathbuf += "Z" + + path.append(Path()) + path[-1].parse(pathbuf) + # Apply the scaling then the translation + translate = Matrix([1,0,0,1,offset.x,-size+attrib.origin.y]) * Matrix([scale,0,0,scale,0,0]) + # This queues the translations until .transform() is called + path[-1].matrix = translate * path[-1].matrix + #path[-1].getTransformations({"transform":"translate({},{}) scale({})".format( + # offset.x, -size+attrib.origin.y, scale)}) + offset.x += (scale*glf.width) + + self.paths.append(path) + if auto_transform: + self.transform() + + def bbox(self): + if self.paths is None or len(self.paths) == 0: + return [Point(0,0),Point(0,0)] + + bboxes = [path.bbox() for paths in self.paths for path in paths] + + return [ + Point(min(bboxes, key=lambda v: v[0].x)[0].x, min(bboxes, key=lambda v: v[0].y)[0].y), + Point(max(bboxes, key=lambda v: v[1].x)[1].x, max(bboxes, key=lambda v: v[1].y)[1].y), + ] + + def transform(self, matrix=None): + if matrix is None: + matrix = self.matrix + else: + matrix *= self.matrix + self.origin = matrix * self.origin + for paths in self.paths: + for path in paths: + path.transform(matrix) + + def segments(self, precision=0): + segs = [] + for paths in self.paths: + for path in paths: + segs.extend(path.segments(precision)) + return segs + + @staticmethod + def load_system_fonts(): + if len(Text._system_fonts.keys()) < 1: + fonts_files = [] + logging.info("Found element. Loading system fonts.") + for path in Text._os_font_paths[platform.system()]: + try: fonts_files.extend([os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser(path)) for f in fn]) + except: pass + + for ffile in fonts_files: + try: + font = ttFont.TTFont(ffile) + name = font["name"].getName(1,1,0).toStr() + style = font["name"].getName(2,1,0).toStr() + if Text._system_fonts.get(name) is None: + Text._system_fonts[name] = {style:ffile} + elif Text._system_fonts[name].get(style) is None: + Text._system_fonts[name][style] = ffile + except:pass + logging.debug(f" Found {len(Text._system_fonts.keys())} fonts in system") + return Text._system_fonts + + # overwrite JSONEncoder for svg classes which have defined a .json() method class JSONEncoder(json.JSONEncoder): def default(self, obj): diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 43aa671..d29df7c 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1,7 +1,7 @@ import argparse, datetime import shlex, os, sys, re -import logging +import logging, io, time import svg2mod.svg as svg import svg2mod.coloredlogger as coloredlogger @@ -236,6 +236,13 @@ def q_next( self, q ): self.p = self.q self.q = q + def __eq__(self, other): + return ( + isinstance(other, LineSegment) and + other.p.x == self.p.x and other.p.y == self.p.y and + other.q.x == self.q.x and other.q.y == self.q.y + ) + #------------------------------------------------------------------------ @@ -257,49 +264,136 @@ def __init__( self, points): # KiCad will not "pick up the pen" when moving between a polygon outline # and holes within it, so we search for a pair of points connecting the - # outline (self) to the hole such that the connecting segment will not - # cross the visible inner space within any hole. + # outline (self) or other previously inserted points to the hole such + # that the connecting segment will not cross the visible inner space + # within any hole. def _find_insertion_point( self, hole, holes, other_insertions ): - # Try the next point on the container: - for cp in range( len( self.points ) ): - container_point = self.points[ cp ] - - # Try the next point on the hole: - for hp in range( len( hole.points ) - 1 ): - hole_point = hole.points[ hp ] - - bridge = LineSegment( container_point, hole_point ) - - # Check if bridge passes over other bridges that will be created - bad_point = False - for index, insertion in other_insertions: - insert = LineSegment( self.points[index], insertion[0]) - if bridge.intersects(insert): - bad_point = True - if bad_point: - continue - - # Check for intersection with each other hole: - for other_hole in holes: - - # If the other hole intersects, don't bother checking - # remaining holes: - if other_hole.intersects( - bridge, - check_connects = ( - other_hole == hole or other_hole == self - ) - ):break - - else: - logging.debug( " Found insertion point: {}, {}".format( cp, hp ) ) - - # No other holes intersected, so this insertion point - # is acceptable: - return ( cp, hole.points_starting_on_index( hp ) ) + connected = list( zip(*other_insertions) ) + if len(connected) > 0: + connected = [self] + list( connected[2] ) + else: + connected = [self] + + for hp in range( len(hole.points) - 1 ): + for poly in connected: + for pp in range( len(poly.points ) - 1 ): + hole_point = hole.points[hp] + bridge = LineSegment( poly.points[pp], hole_point) + trying_new_point = True + second_bridge = None + connected_poly = poly + while trying_new_point: + trying_new_point = False + + # Check if bridge passes over other bridges that will be created + bad_point = False + for ip, insertion, con_poly in other_insertions: + insert = LineSegment( ip, insertion[0]) + if bridge.intersects(insert): + bad_point = True + break + + if bad_point: + continue + + # Check for intersection with each other hole: + for other_hole in holes: + + # If the other hole intersects, don't bother checking + # remaining holes: + res = other_hole.intersects( + bridge, + check_connects = ( + other_hole == hole or connected.count( other_hole ) > 0 + ), + get_points = ( + other_hole == hole or connected.count( other_hole ) > 0 + ) + ) + if isinstance(res, bool) and res: break + elif isinstance(res, tuple) and len(res) != 0: + trying_new_point = True + connected_poly = other_hole + if other_hole == hole: + hole_point = res[0] + bridge = LineSegment( bridge.p, res[0] ) + second_bridge = LineSegment( bridge.p, res[1] ) + else: + bridge = LineSegment( res[0], hole_point ) + second_bridge = LineSegment( res[1], hole_point ) + break + + + else: + # logging.critical( " Found insertion point: {}, {}".format( bridge.p, hole.points.index(hole_point) ) ) + logging.info( "[{}, {}]".format( bridge.p, hole_point ) ) + + # No other holes intersected, so this insertion point + # is acceptable: + return ( bridge.p, hole.points_starting_on_index( hole.points.index(hole_point) ), hole ) + + if second_bridge and not trying_new_point: + bridge = second_bridge + if hole_point != bridge.q: + hole_point = bridge.q + second_bridge = None + trying_new_point = True + + + + ## Try the next point on the container: + #for cp in range( len( self.points ) ): + # container_point = self.points[ cp ] + + # # Try the next point on the hole: + # for hp in range( len( hole.points ) - 1 ): + # hole_point = hole.points[ hp ] + + # bridge = LineSegment( container_point, hole_point ) + + # # Check if bridge passes over other bridges that will be created + # bad_point = False + # for index, insertion, hi in other_insertions: + # insert = LineSegment( self.points[index], insertion[0]) + # if bridge.intersects(insert): + # bad_point = True + # if bad_point: + # continue + + # # Check for intersection with each other hole: + # for index, other_hole, hole_object in other_insertions: + + # # If the other hole intersects, don't bother checking + # # remaining holes: + # if hole_object.intersects( + # bridge, + # check_connects = ( + # other_hole == hole or other_hole == self + # ) + # ):break + + # # Check for intersection with each other hole: + # for other_hole in holes: + + # # If the other hole intersects, don't bother checking + # # remaining holes: + # if other_hole.intersects( + # bridge, + # check_connects = ( + # other_hole == hole or other_hole == self + # ) + # ):break + + # else: + # logging.debug( " Found insertion point: {}, {}".format( cp, hp ) ) + + # # No other holes intersected, so this insertion point + # # is acceptable: + # return ( cp, hole.points_starting_on_index( hp ), holes[holes.index(hole)] ) logging.error("Could not insert segment without overlapping other segments") + exit(1) #------------------------------------------------------------------------ @@ -346,44 +440,46 @@ def inline( self, segments ): if insertion is not None: insertions.append( insertion ) - insertions.sort( key = lambda i: i[ 0 ] ) + # insertions.sort( key = lambda i: i[ 0 ] ) - inlined = [ self.points[ 0 ] ] - ip = 1 - points = self.points + # inlined = [ self.points[ 0 ] ] + # ip = 1 + points = self.points[ : ] for insertion in insertions: - while ip <= insertion[ 0 ]: - inlined.append( points[ ip ] ) - ip += 1 + ip = points.index(insertion[0]) + # while inlined[-1] != insertion[ 0 ]: + # inlined.append( points.pop(0) ) if ( - inlined[ -1 ].x == insertion[ 1 ][ 0 ].x and - inlined[ -1 ].y == insertion[ 1 ][ 0 ].y + points[ ip ].x == insertion[ 1 ][ 0 ].x and + points[ ip ].y == insertion[ 1 ][ 0 ].y ): - inlined += insertion[ 1 ][ 1 : -1 ] + points = points[:ip+1] + insertion[ 1 ][ 1 : -1 ] + points[ip:] else: - inlined += insertion[ 1 ] - - inlined.append( svg.Point( - points[ ip - 1 ].x, - points[ ip - 1 ].y, - ) ) + points = points[:ip+1] + insertion[ 1 ] + points[ip:] - while ip < len( points ): - inlined.append( points[ ip ] ) - ip += 1 + # inlined.append( svg.Point( + # points[ ip - 1 ].x, + # points[ ip - 1 ].y, + # ) ) +# + # while ip < len( points ): + # inlined.append( points[ ip ] ) + # ip += 1 - return inlined + return points #------------------------------------------------------------------------ - def intersects( self, line_segment, check_connects ): + def intersects( self, line_segment, check_connects , count_intersections=False, get_points=False): hole_segment = LineSegment() + intersections = 0 + # Check each segment of other hole for intersection: for point in self.points: @@ -400,8 +496,19 @@ def intersects( self, line_segment, check_connects ): #print( "Intersection detected." ) - return True - + if count_intersections: + # If line_segment passes through a point this prevents a second false positive + hole_segment.q = None + intersections += 1 + elif get_points: + return hole_segment.p, hole_segment.q + else: + return True + + if count_intersections: + return intersections + if get_points: + return () return False @@ -449,6 +556,40 @@ def process( self, transformer, flip, fill ): #------------------------------------------------------------------------ + # Calculate bounding box of self + def bounding_box(self): + self.bbox = [ + svg.Point(min(self.points, key=lambda v: v.x).x, min(self.points, key=lambda v: v.y).y), + svg.Point(max(self.points, key=lambda v: v.x).x, max(self.points, key=lambda v: v.y).y), + ] + + #------------------------------------------------------------------------ + + # Checks if the supplied polygon either contains or insets our bounding box + def are_distinct(self, polygon): + distinct = True + + smaller = min([self, polygon], key=lambda p: svg.Segment(p.bbox[0], p.bbox[1]).length()) + larger = self if smaller == polygon else polygon + + if ( + larger.bbox[0].x < smaller.bbox[0].x and + larger.bbox[0].y < smaller.bbox[0].y and + larger.bbox[1].x > smaller.bbox[1].x and + larger.bbox[1].y > smaller.bbox[1].y + ): + distinct = False + + # Check number of horizontal intersections. If the number is odd then it the smaller polygon + # is contained. If the number is even then the polygon is outside of the larger polygon + if not distinct: + tline = LineSegment(smaller.points[0], svg.Point(larger.bbox[1].x, smaller.points[0].y)) + distinct = bool((larger.intersects(tline, False, True) + 1)%2) + + return distinct + #------------------------------------------------------------------------ + + #---------------------------------------------------------------------------- class Svg2ModImport( object ): @@ -475,16 +616,17 @@ def _prune_hidden( self, items = None ): if(item.items): self._prune_hidden( item.items ) - def __init__( self, file_name, module_name, module_value, ignore_hidden_layers ): + def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", ignore_hidden_layers=False ): self.file_name = file_name self.module_name = module_name self.module_value = module_value - logging.getLogger("unfiltered").info( "Parsing SVG..." ) + if file_name: + logging.getLogger("unfiltered").info( "Parsing SVG..." ) - self.svg = svg.parse( file_name) - logging.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) + self.svg = svg.parse( file_name) + logging.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) if( ignore_hidden_layers ): self._prune_hidden() @@ -521,7 +663,7 @@ def _get_fill_stroke( self, item ): for property in filter(None, item.style.split( ";" )): - nv = property.split( ":" ); + nv = property.split( ":" ) name = nv[ 0 ].strip() value = nv[ 1 ].strip() @@ -563,9 +705,9 @@ def _get_fill_stroke( self, item ): def __init__( self, - svg2mod_import, - file_name, - center, + svg2mod_import = Svg2ModImport(), + file_name = None, + center = False, scale_factor = 1.0, precision = 20.0, use_mm = True, @@ -591,6 +733,19 @@ def __init__( #------------------------------------------------------------------------ + def add_svg_element(self, elem, layer="F.SilkS"): + grp = svg.Group() + grp.name = layer + grp.items.append(elem) + try: + self.imported.svg.items.append(grp) + except AttributeError: + self.imported.svg = svg.Svg() + self.imported.svg.items.append(grp) + + #------------------------------------------------------------------------ + + def _calculate_translation( self ): min_point, max_point = self.imported.svg.bbox() @@ -655,7 +810,8 @@ def _write_items( self, items, layer, flip = False ): elif ( isinstance( item, svg.Path ) or isinstance( item, svg.Ellipse) or - isinstance( item, svg.Rect ) + isinstance( item, svg.Rect ) or + isinstance( item, svg.Text ) ): segments = [ @@ -666,19 +822,46 @@ def _write_items( self, items, layer, flip = False ): ] fill, stroke, stroke_width = self._get_fill_stroke( item ) + fill = (False if layer == "Edge.Cuts" else fill) for segment in segments: segment.process( self, flip, fill ) if len( segments ) > 1: - points = segments[ 0 ].inline( segments[ 1 : ] ) + [poly.bounding_box() for poly in segments] + segments.sort(key=lambda v: svg.Segment(v.bbox[0], v.bbox[1]).length(), reverse=True) + + while len(segments) > 0: + inlinable = [segments[0]] + for seg in segments[1:]: + if not inlinable[0].are_distinct(seg): + append = True + if len(inlinable) > 1: + for hole in inlinable[1:]: + if not hole.are_distinct(seg): + append = False + break + if append: inlinable.append(seg) + for poly in inlinable: + segments.pop(segments.index(poly)) + if len(inlinable) > 1: + points = inlinable[ 0 ].inline( inlinable[ 1 : ] ) + elif len(inlinable) > 0: + points = inlinable[ 0 ].points + + logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) + + self._write_polygon( + points, layer, fill, stroke, stroke_width + ) + continue elif len( segments ) > 0: points = segments[ 0 ].points - if len ( segments ) != 0: + if len ( segments ) == 1: - logging.info( " Writing polygon with {} points".format(len( points ) )) + logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) self._write_polygon( points, layer, fill, stroke, stroke_width @@ -790,13 +973,19 @@ def write( self, cmdline ): # Must come after pruning: self._calculate_translation() - logging.getLogger("unfiltered").info( "Writing module file: {}".format( self.file_name ) ) - self.output_file = open( self.file_name, 'w' ) + if self.file_name: + logging.getLogger("unfiltered").info( "Writing module file: {}".format( self.file_name ) ) + self.output_file = open( self.file_name, 'w' ) + else: + self.output_file = io.StringIO() self._write_library_intro(cmdline) self._write_modules() + if self.file_name is None: + self.raw_file_data = self.output_file.getvalue() + self.output_file.close() self.output_file = None @@ -901,7 +1090,7 @@ def _write_library_intro( self, cmdline ): datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ), units, modules_list, - cmdline + cmdline.replace("\\","\\\\") ) ) @@ -1295,10 +1484,10 @@ def _write_library_intro( self, cmdline ): (tags {3}) """.format( self.imported.module_name, #0 - int( round( os.path.getctime( #1 - self.imported.file_name - ) ) ), - "Converted using: {}".format( cmdline ), #2 + int( round( #1 + os.path.getctime( self.imported.file_name ) if self.imported.file_name else time.time() + ) ), + "Converted using: {}".format( cmdline.replace("\\", "\\\\") ), #2 "svg2mod", #3 ) ) From 9f241743aa981dddc0597a18f47c769b464d2753 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 12 Mar 2021 23:13:10 -0700 Subject: [PATCH 081/151] Clean up comments and fix readme links --- README.md | 4 +-- svg2mod/svg2mod.py | 68 ---------------------------------------------- 2 files changed, 2 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 366bb74..744b660 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # svg2mod -[![Build Status](https://travis-ci.com/svg2mod/svg2mod.svg?branch=master)](https://travis-ci.com/svg2mod/svg2mod) -[![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod)](https://github.com/svg2mod/svg2mod/commits/master) +[![Build Status](https://travis-ci.com/svg2mod/svg2mod.svg?branch=main)](https://travis-ci.com/svg2mod/svg2mod) +[![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod)](https://github.com/svg2mod/svg2mod/commits/main) [![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=black)](https://pypi.org/project/svg2mod/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod)](https://pypi.org/project/svg2mod/) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index d29df7c..8ff4f46 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -326,7 +326,6 @@ def _find_insertion_point( self, hole, holes, other_insertions ): else: - # logging.critical( " Found insertion point: {}, {}".format( bridge.p, hole.points.index(hole_point) ) ) logging.info( "[{}, {}]".format( bridge.p, hole_point ) ) # No other holes intersected, so this insertion point @@ -340,58 +339,6 @@ def _find_insertion_point( self, hole, holes, other_insertions ): second_bridge = None trying_new_point = True - - - ## Try the next point on the container: - #for cp in range( len( self.points ) ): - # container_point = self.points[ cp ] - - # # Try the next point on the hole: - # for hp in range( len( hole.points ) - 1 ): - # hole_point = hole.points[ hp ] - - # bridge = LineSegment( container_point, hole_point ) - - # # Check if bridge passes over other bridges that will be created - # bad_point = False - # for index, insertion, hi in other_insertions: - # insert = LineSegment( self.points[index], insertion[0]) - # if bridge.intersects(insert): - # bad_point = True - # if bad_point: - # continue - - # # Check for intersection with each other hole: - # for index, other_hole, hole_object in other_insertions: - - # # If the other hole intersects, don't bother checking - # # remaining holes: - # if hole_object.intersects( - # bridge, - # check_connects = ( - # other_hole == hole or other_hole == self - # ) - # ):break - - # # Check for intersection with each other hole: - # for other_hole in holes: - - # # If the other hole intersects, don't bother checking - # # remaining holes: - # if other_hole.intersects( - # bridge, - # check_connects = ( - # other_hole == hole or other_hole == self - # ) - # ):break - - # else: - # logging.debug( " Found insertion point: {}, {}".format( cp, hp ) ) - - # # No other holes intersected, so this insertion point - # # is acceptable: - # return ( cp, hole.points_starting_on_index( hp ), holes[holes.index(hole)] ) - logging.error("Could not insert segment without overlapping other segments") exit(1) @@ -440,17 +387,11 @@ def inline( self, segments ): if insertion is not None: insertions.append( insertion ) - # insertions.sort( key = lambda i: i[ 0 ] ) - - # inlined = [ self.points[ 0 ] ] - # ip = 1 points = self.points[ : ] for insertion in insertions: ip = points.index(insertion[0]) - # while inlined[-1] != insertion[ 0 ]: - # inlined.append( points.pop(0) ) if ( points[ ip ].x == insertion[ 1 ][ 0 ].x and @@ -460,15 +401,6 @@ def inline( self, segments ): else: points = points[:ip+1] + insertion[ 1 ] + points[ip:] - # inlined.append( svg.Point( - # points[ ip - 1 ].x, - # points[ ip - 1 ].y, - # ) ) -# - # while ip < len( points ): - # inlined.append( points[ ip ] ) - # ip += 1 - return points From 71e35e5c7cbc6df678e6a60b7f000ab906216ccc Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 12 Mar 2021 23:25:41 -0700 Subject: [PATCH 082/151] Add default value to write() function --- svg2mod/svg2mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 8ff4f46..c69a3d3 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -898,7 +898,7 @@ def transform_point( self, point, flip = False ): #------------------------------------------------------------------------ - def write( self, cmdline ): + def write( self, cmdline="" ): self._prune() From 2bce065395ee225d32e03db3c80d8843d6e44773 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 12 Mar 2021 23:45:10 -0700 Subject: [PATCH 083/151] Fix setup.py to have fonttools as dep Also make the dirty tag be PEP 440 compliant --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index dd202f2..3d2fd74 100644 --- a/setup.py +++ b/setup.py @@ -18,10 +18,12 @@ try: tag = os.popen("git describe --tag")._stream.read().strip() + tag = tag.replace("-", ".dev", 1).replace("-", "+") except: tag = "development" requirements = [ + "fonttools" ] test_requirements = [ From 3acbce2b3c8bfff1ec7e7e458561a00543a05255 Mon Sep 17 00:00:00 2001 From: michaelj Date: Mon, 15 Mar 2021 12:28:02 -0600 Subject: [PATCH 084/151] add linter --- .lint/custom_dict | 17 + .lint/general_rc | 591 ++++++++++++++++++++++++++++ .lint/spelling_rc | 870 +++++++++++++++++++++++++++++++++++++++++ setup.py | 9 +- svg2mod/svg/svg/svg.py | 16 +- 5 files changed, 1493 insertions(+), 10 deletions(-) create mode 100644 .lint/custom_dict create mode 100644 .lint/general_rc create mode 100644 .lint/spelling_rc diff --git a/.lint/custom_dict b/.lint/custom_dict new file mode 100644 index 0000000..466148c --- /dev/null +++ b/.lint/custom_dict @@ -0,0 +1,17 @@ +svg +inkscape +kicad +stderr +stdout +vect +rx +ry +json +px +bezier +txt +cjlano +sys +Ramer +Douglas +Peucker diff --git a/.lint/general_rc b/.lint/general_rc new file mode 100644 index 0000000..cbb8bd2 --- /dev/null +++ b/.lint/general_rc @@ -0,0 +1,591 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=invalid-name, + no-member, + print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA +# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA +# (hunspell). +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.lint/spelling_rc b/.lint/spelling_rc new file mode 100644 index 0000000..6071865 --- /dev/null +++ b/.lint/spelling_rc @@ -0,0 +1,870 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=blacklisted-name, + invalid-name, + empty-docstring, + unneeded-not, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + singleton-comparison, + misplaced-comparison-constant, + unidiomatic-typecheck, + non-ascii-name, + consider-using-enumerate, + consider-iterating-dictionary, + bad-classmethod-argument, + bad-mcs-method-argument, + bad-mcs-classmethod-argument, + single-string-used-for-slots, + line-too-long, + too-many-lines, + trailing-whitespace, + missing-final-newline, + trailing-newlines, + multiple-statements, + superfluous-parens, + mixed-line-endings, + unexpected-line-ending-format, + multiple-imports, + wrong-import-order, + ungrouped-imports, + wrong-import-position, + useless-import-alias, + import-outside-toplevel, + len-as-condition, + syntax-error, + unrecognized-inline-option, + bad-option-value, + init-is-generator, + return-in-init, + function-redefined, + not-in-loop, + return-outside-function, + yield-outside-function, + return-arg-in-generator, + nonexistent-operator, + duplicate-argument-name, + abstract-class-instantiated, + bad-reversed-sequence, + too-many-star-expressions, + invalid-star-assignment-target, + star-needs-assignment-target, + nonlocal-and-global, + continue-in-finally, + nonlocal-without-binding, + used-prior-global-declaration, + misplaced-format-function, + method-hidden, + access-member-before-definition, + no-method-argument, + no-self-argument, + invalid-slots-object, + assigning-non-slot, + invalid-slots, + inherit-non-class, + inconsistent-mro, + duplicate-bases, + class-variable-slots-conflict, + non-iterator-returned, + unexpected-special-method-signature, + invalid-length-returned, + invalid-bool-returned, + invalid-index-returned, + invalid-repr-returned, + invalid-str-returned, + invalid-bytes-returned, + invalid-hash-returned, + invalid-length-hint-returned, + invalid-format-returned, + invalid-getnewargs-returned, + invalid-getnewargs-ex-returned, + import-error, + relative-beyond-top-level, + used-before-assignment, + undefined-variable, + undefined-all-variable, + invalid-all-object, + no-name-in-module, + unpacking-non-sequence, + bad-except-order, + raising-bad-type, + bad-exception-context, + misplaced-bare-raise, + raising-non-exception, + notimplemented-raised, + catching-non-exception, + bad-super-call, + no-member, + not-callable, + assignment-from-no-return, + no-value-for-parameter, + too-many-function-args, + unexpected-keyword-arg, + redundant-keyword-arg, + missing-kwoa, + invalid-sequence-index, + invalid-slice-index, + assignment-from-none, + not-context-manager, + invalid-unary-operand-type, + unsupported-binary-operation, + repeated-keyword, + not-an-iterable, + not-a-mapping, + unsupported-membership-test, + unsubscriptable-object, + unsupported-assignment-operation, + unsupported-delete-operation, + invalid-metaclass, + unhashable-dict-key, + dict-iter-missing-items, + logging-unsupported-format, + logging-format-truncated, + logging-too-many-args, + logging-too-few-args, + bad-format-character, + truncated-format-string, + mixed-format-string, + format-needs-mapping, + missing-format-string-key, + too-many-format-args, + too-few-format-args, + bad-string-format-type, + bad-str-strip-call, + invalid-envvar-value, + print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + yield-inside-async-function, + not-async-context-manager, + fatal, + astroid-error, + parse-error, + method-check-failed, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + c-extension-no-member, + literal-comparison, + comparison-with-itself, + no-self-use, + no-classmethod-decorator, + no-staticmethod-decorator, + useless-object-inheritance, + property-with-parameters, + cyclic-import, + duplicate-code, + too-many-ancestors, + too-many-instance-attributes, + too-few-public-methods, + too-many-public-methods, + too-many-return-statements, + too-many-branches, + too-many-arguments, + too-many-locals, + too-many-statements, + too-many-boolean-expressions, + consider-merging-isinstance, + too-many-nested-blocks, + simplifiable-if-statement, + redefined-argument-from-local, + no-else-return, + consider-using-ternary, + trailing-comma-tuple, + stop-iteration-return, + simplify-boolean-expression, + inconsistent-return-statements, + useless-return, + consider-swap-variables, + consider-using-join, + consider-using-in, + consider-using-get, + chained-comparison, + consider-using-dict-comprehension, + consider-using-set-comprehension, + simplifiable-if-expression, + no-else-raise, + unnecessary-comprehension, + consider-using-sys-exit, + no-else-break, + no-else-continue, + super-with-arguments, + unreachable, + dangerous-default-value, + pointless-statement, + pointless-string-statement, + expression-not-assigned, + unnecessary-pass, + unnecessary-lambda, + duplicate-key, + assign-to-new-keyword, + useless-else-on-loop, + exec-used, + eval-used, + confusing-with-statement, + using-constant-test, + missing-parentheses-for-call-in-test, + self-assigning-variable, + redeclared-assigned-name, + assert-on-string-literal, + comparison-with-callable, + lost-exception, + assert-on-tuple, + attribute-defined-outside-init, + bad-staticmethod-argument, + protected-access, + arguments-differ, + signature-differs, + abstract-method, + super-init-not-called, + no-init, + non-parent-init-called, + useless-super-delegation, + invalid-overridden-method, + unnecessary-semicolon, + bad-indentation, + wildcard-import, + deprecated-module, + reimported, + import-self, + preferred-module, + misplaced-future, + fixme, + global-variable-undefined, + global-variable-not-assigned, + global-statement, + global-at-module-level, + unused-import, + unused-variable, + unused-argument, + unused-wildcard-import, + redefined-outer-name, + redefined-builtin, + redefine-in-handler, + undefined-loop-variable, + unbalanced-tuple-unpacking, + cell-var-from-loop, + possibly-unused-variable, + self-cls-assignment, + bare-except, + broad-except, + duplicate-except, + try-except-raise, + raise-missing-from, + binary-op-exception, + raising-format-tuple, + wrong-exception-operation, + keyword-arg-before-vararg, + arguments-out-of-order, + non-str-assignment-to-dunder-name, + isinstance-second-argument-not-valid-type, + logging-not-lazy, + logging-format-interpolation, + logging-fstring-interpolation, + bad-format-string-key, + unused-format-string-key, + bad-format-string, + missing-format-argument-key, + unused-format-string-argument, + format-combined-specification, + missing-format-attribute, + invalid-format-index, + duplicate-string-formatting-argument, + f-string-without-interpolation, + anomalous-backslash-in-string, + anomalous-unicode-escape-in-string, + implicit-str-concat, + inconsistent-quotes, + bad-open-mode, + boolean-datetime, + redundant-unittest-assert, + deprecated-method, + bad-thread-instantiation, + shallow-copy-environ, + invalid-envvar-default, + subprocess-popen-preexec-fn, + subprocess-run-check, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=wrong-spelling-in-comment, + wrong-spelling-in-docstring, + invalid-characters-in-docstring + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA +# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA +# (hunspell). +spelling-dict=en_US + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file=.lint/custom_dict + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/setup.py b/setup.py index 3d2fd74..1e11b6e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import os +import subprocess import setuptools @@ -17,10 +17,11 @@ tag = "" try: - tag = os.popen("git describe --tag")._stream.read().strip() + tag = subprocess.check_output(["git","describe","--tag"], stderr=subprocess.STDOUT) + tag = tag.stdout.decode('utf-8') tag = tag.replace("-", ".dev", 1).replace("-", "+") -except: - tag = "development" +except (FileNotFoundError, subprocess.CalledProcessError) as e: + tag = "0.dev0" requirements = [ "fonttools" diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index b7e4a38..4b7efac 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -1144,13 +1144,15 @@ def segments(self, precision=0): return segs @staticmethod - def load_system_fonts(): - if len(Text._system_fonts.keys()) < 1: + def load_system_fonts(reload=False): + if len(Text._system_fonts.keys()) < 1 or reload: fonts_files = [] - logging.info("Found element. Loading system fonts.") + logging.info("Loading system fonts.") for path in Text._os_font_paths[platform.system()]: - try: fonts_files.extend([os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser(path)) for f in fn]) - except: pass + try: + fonts_files.extend([os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser(path)) for f in fn]) + except: + pass for ffile in fonts_files: try: @@ -1161,7 +1163,8 @@ def load_system_fonts(): Text._system_fonts[name] = {style:ffile} elif Text._system_fonts[name].get(style) is None: Text._system_fonts[name][style] = ffile - except:pass + except: + pass logging.debug(f" Found {len(Text._system_fonts.keys())} fonts in system") return Text._system_fonts @@ -1188,3 +1191,4 @@ def default(self, obj): tag = getattr(cls, 'tag', None) if tag: svgClass[svg_ns + tag] = cls + From 7930c1c39c84a7b80d40cae36e46eb95fd22bc39 Mon Sep 17 00:00:00 2001 From: michaelj Date: Mon, 15 Mar 2021 12:56:19 -0600 Subject: [PATCH 085/151] update spellings --- .lint/custom_dict | 4 ++++ .lint/general_rc | 3 ++- svg2mod/coloredlogger.py | 6 +++--- svg2mod/svg/svg/geometry.py | 12 +++++++----- svg2mod/svg/svg/svg.py | 34 +++++++++++++++++++++------------- svg2mod/svg2mod.py | 16 ++++++++-------- 6 files changed, 45 insertions(+), 30 deletions(-) diff --git a/.lint/custom_dict b/.lint/custom_dict index 466148c..f8a0557 100644 --- a/.lint/custom_dict +++ b/.lint/custom_dict @@ -15,3 +15,7 @@ sys Ramer Douglas Peucker +Traceback +precompute +pdistance +elt diff --git a/.lint/general_rc b/.lint/general_rc index cbb8bd2..53c2022 100644 --- a/.lint/general_rc +++ b/.lint/general_rc @@ -60,7 +60,8 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=invalid-name, +disable=bare-except, + invalid-name, no-member, print-statement, parameter-unpacking, diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index f10b306..9bd8f5a 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -24,8 +24,8 @@ def format(self, record): self._style._fmt = fmt_org return result -# This will split logging messegaes at breakpoint. Anything higher will be sent -# to sys.stderr and everything else to sys.stdout +# This will split logging messages at the specified break point. Anything higher +# will be sent to sys.stderr and everything else to sys.stdout def split_logger(logger, formatter=Formatter(), breakpoint=logging.WARNING): hdlrerr = logging.StreamHandler(sys.stderr) hdlrerr.addFilter(lambda msg: breakpoint <= msg.levelno) @@ -38,4 +38,4 @@ def split_logger(logger, formatter=Formatter(), breakpoint=logging.WARNING): logger.addHandler(hdlrerr) logger.addHandler(hdlrout) - \ No newline at end of file + diff --git a/svg2mod/svg/svg/geometry.py b/svg2mod/svg/svg/geometry.py index 2d2397e..d4f1f32 100644 --- a/svg2mod/svg/svg/geometry.py +++ b/svg2mod/svg/svg/geometry.py @@ -1,5 +1,5 @@ # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > -# Copyright (C) 2021 -- svg2mod developers < github.com / svg2mod > +# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,7 +20,9 @@ related to SVG parsing. It can be reused outside the scope of SVG. ''' -import math, numbers, operator +import math +import numbers +import operator class Point: def __init__(self, x=None, y=None): @@ -63,7 +65,7 @@ def __add__(self, other): return Point(self.x + other.x, self.y + other.y) def __sub__(self, other): - '''Substract two Points. + '''Subtract two Points. >>> Point(1,2) - Point(3,2) (-2.000,0.000) ''' @@ -249,7 +251,7 @@ def rbbox(self): return (Point(xmin,ymin), Point(xmax,ymax)) def segments(self, precision=0): - '''Return a polyline approximation ("segments") of the Bezier curve + '''Return a poly-line approximation ("segments") of the Bezier curve precision is the minimum significative length of a segment''' segments = [] # n is the number of Bezier points to draw according to precision @@ -312,7 +314,7 @@ def simplify_segment(segment, epsilon): key=operator.itemgetter(1)) if maxDist > epsilon: - # Recursively call with segment splited in 2 on its furthest point + # Recursively call with segment split in 2 on its furthest point r1 = simplify_segment(segment[:index+1], epsilon) r2 = simplify_segment(segment[index:], epsilon) # Remove redundant 'middle' Point diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 4b7efac..6876872 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -19,17 +19,25 @@ from __future__ import absolute_import import xml.etree.ElementTree as etree -import traceback, math -import sys, os, copy, re -import itertools, operator +import math +import sys +import os +import copy +import re +import itertools +import operator import platform -import json, logging -from .geometry import * +import json +import logging +import inspect + from fontTools.ttLib import ttFont from fontTools.pens.recordingPen import RecordingPen from fontTools.pens.basePen import decomposeQuadraticSegment from fontTools.misc import loggingTools +from .geometry import * + # Make fontTools more quiet loggingTools.configLogger(level=logging.INFO) @@ -468,7 +476,7 @@ def parse(self, pathstr): else: pt0 = current_pt pt1 = current_pt - # Symetrical of pt1 against pt0 + # Symmetrical of pt1 against pt0 bezier_pts.append(pt1 + pt1 - pt0) for i in range(0,nbpts[command]): @@ -486,7 +494,7 @@ def parse(self, pathstr): rx = pathlst.pop() ry = pathlst.pop() xrot = pathlst.pop() - # Arc flags are not necesarily sepatated numbers + # Arc flags are not necessarily separated numbers flags = pathlst.pop().strip() large_arc_flag = flags[0] if large_arc_flag not in '01': @@ -611,7 +619,7 @@ def segments(self, precision=0): def simplify(self, precision): return self -# An arc is an ellipse with a begining and an end point instead of an entire circumference +# An arc is an ellipse with a beginning and an end point instead of an entire circumference class Arc(Ellipse): '''SVG ''' # class Ellipse handles the tag @@ -701,7 +709,7 @@ def calcuate_center(self): else: xroots = [(-qb+math.sqrt(root))/(2*qa), (-qb-math.sqrt(root))/(2*qa)] points = [Point(xroots[0], xroots[0]*m + b), Point(xroots[1], xroots[1]*m + b)] - # Calculate the angle of the begining point to the end point + # Calculate the angle of the beginning point to the end point # If counterclockwise the two angles are the angle is within 180 degrees of each other: # and no flags are set use the first center @@ -733,7 +741,7 @@ def calcuate_center(self): point = Point(round(point.x, 10), round(point.y, 10)) self.center = point - # Calculate start and end angle of the unrotated arc + # Calculate start and end angle of the un-rotated arc if len(self.angles) < 2: self.angles = [] for pt in self.end_pts: @@ -755,7 +763,7 @@ def segments(self, precision=0): def P(self, t): '''Return a Point on the Arc for t in [0..1]''' - #TODO change point cords if rotaion + #TODO change point cords if rotation is set x = self.center.x + self.rx * math.cos(((self.angles[1] - self.angles[0]) * t) + self.angles[0]) y = self.center.y + self.ry * math.sin(((self.angles[1] - self.angles[0]) * t) + self.angles[0]) return Point(x,y) @@ -948,7 +956,7 @@ def parse(self, elt, parent): x = elt.get('x') y = elt.get('y') - # It seems that any values in style that override these values take precidence + # It seems that any values in style that override these values take precedence self.font_configs = { "font-family": elt.get('font-family'), "font-size": elt.get('font-size'), @@ -1184,7 +1192,7 @@ def default(self, obj): # SVG tag handler classes are initialized here # (classes must be defined before) -import inspect + svgClass = {} # Register all classes with attribute 'tag' in svgClass dict for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index c69a3d3..343c429 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -134,7 +134,7 @@ class LineSegment( object ): @staticmethod def _on_segment( p, q, r ): - """ Given three colinear points p, q, and r, check if + """ Given three collinear points p, q, and r, check if point q lies on line segment pr. """ if ( @@ -154,7 +154,7 @@ def _on_segment( p, q, r ): def _orientation( p, q, r ): """ Find orientation of ordered triplet (p, q, r). Returns following values - 0 --> p, q and r are colinear + 0 --> p, q and r are collinear 1 --> Clockwise 2 --> Counterclockwise """ @@ -209,22 +209,22 @@ def intersects( self, segment ): or - # p1, q1 and p2 are colinear and p2 lies on segment p1q1: + # p1, q1 and p2 are collinear and p2 lies on segment p1q1: ( o1 == 0 and self._on_segment( self.p, segment.p, self.q ) ) or - # p1, q1 and p2 are colinear and q2 lies on segment p1q1: + # p1, q1 and p2 are collinear and q2 lies on segment p1q1: ( o2 == 0 and self._on_segment( self.p, segment.q, self.q ) ) or - # p2, q2 and p1 are colinear and p1 lies on segment p2q2: + # p2, q2 and p1 are collinear and p1 lies on segment p2q2: ( o3 == 0 and self._on_segment( segment.p, self.p, segment.q ) ) or - # p2, q2 and q1 are colinear and q1 lies on segment p2q2: + # p2, q2 and q1 are collinear and q1 lies on segment p2q2: ( o4 == 0 and self._on_segment( segment.p, self.q, segment.q ) ) ) @@ -617,7 +617,7 @@ def _get_fill_stroke( self, item ): # the top-level viewport_scale scale = self.imported.svg.viewport_scale / self.scale_factor - # remove unessesary presion to reduce floating point errors + # remove unnecessary precession to reduce floating point errors stroke_width = round(stroke_width/scale, 6) elif name == "stroke-opacity": @@ -651,7 +651,7 @@ def __init__( scale_factor *= 25.4 / float(dpi) use_mm = True else: - # PCBNew uses "decimil" (10K DPI); + # PCBNew uses decimal (10K DPI); scale_factor *= 10000.0 / float(dpi) self.imported = svg2mod_import From c681b163be8a641c924ef33a0090848e3ed2409b Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 15 Mar 2021 15:29:30 -0600 Subject: [PATCH 086/151] Make linting more verbose --- .linter/cleanup_rc | 543 +++++++++++++++++++++++++++ .linter/custom_dict | 28 ++ .linter/general_rc | 556 ++++++++++++++++++++++++++++ .linter/spelling_rc | 870 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1997 insertions(+) create mode 100644 .linter/cleanup_rc create mode 100644 .linter/custom_dict create mode 100644 .linter/general_rc create mode 100644 .linter/spelling_rc diff --git a/.linter/cleanup_rc b/.linter/cleanup_rc new file mode 100644 index 0000000..2daa034 --- /dev/null +++ b/.linter/cleanup_rc @@ -0,0 +1,543 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=0.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=all + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member, + anomalous-backslash-in-string, + fixme, + line-too-long, + no-else-return, + no-else-break, + no-else-continue, + no-else-raise, + useless-object-inheritance, + too-few-public-methods, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-nested-blocks, + too-many-statements, + too-many-function-args, + pointless-string-statement, + superfluous-parens, + no-self-use, + signature-differs, + too-many-lines, + wrong-import-order, + useless-import-alias, + continue-in-finally, + duplicate-code, + pointless-statement, + pointless-string-statement, + unused-import, + unused-variable, + unused-argument, + unused-wildcard-import, + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=no + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA +# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA +# (hunspell). +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=new + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.linter/custom_dict b/.linter/custom_dict new file mode 100644 index 0000000..c8110a4 --- /dev/null +++ b/.linter/custom_dict @@ -0,0 +1,28 @@ +svg +inkscape +kicad +stderr +stdout +vect +rx +ry +json +px +bezier +txt +cjlano +sys +Ramer +Douglas +Peucker +Traceback +precompute +pdistance +elt +rect +coord +pc +attrib +viewport +init +collinear diff --git a/.linter/general_rc b/.linter/general_rc new file mode 100644 index 0000000..53d08cb --- /dev/null +++ b/.linter/general_rc @@ -0,0 +1,556 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=all + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member, + missing-function-docstring, + missing-class-docstring, + missing-module-docstring, + trailing-whitespace, + redefined-builtin, + bad-indentation, + unidiomatic-typecheck, + undefined-variable, + no-member, + dangerous-default-value, + expression-not-assigned, + invalid-length-returned, + invalid-bool-returned, + invalid-index-returned, + invalid-repr-returned, + invalid-str-returned, + invalid-bytes-returned, + invalid-hash-returned, + invalid-length-hint-returned, + invalid-format-returned, + invalid-getnewargs-returned, + invalid-getnewargs-ex-returned, + used-before-assignment, + raising-bad-type, + bad-exception-context, + bad-super-call, + not-an-iterable, + no-method-argument, + no-self-argument, + invalid-slots-object, + assigning-non-slot, + invalid-slots, + inherit-non-class, + empty-docstring, + non-ascii-name, + unexpected-line-ending-format, + syntax-error, + unrecognized-inline-option, + return-in-init, + duplicate-argument-name, + abstract-class-instantiated, + not-callable, + assignment-from-no-return, + fatal, + parse-error, + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=no + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA +# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA +# (hunspell). +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.linter/spelling_rc b/.linter/spelling_rc new file mode 100644 index 0000000..a58d56a --- /dev/null +++ b/.linter/spelling_rc @@ -0,0 +1,870 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=blacklisted-name, + invalid-name, + empty-docstring, + unneeded-not, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + singleton-comparison, + misplaced-comparison-constant, + unidiomatic-typecheck, + non-ascii-name, + consider-using-enumerate, + consider-iterating-dictionary, + bad-classmethod-argument, + bad-mcs-method-argument, + bad-mcs-classmethod-argument, + single-string-used-for-slots, + line-too-long, + too-many-lines, + trailing-whitespace, + missing-final-newline, + trailing-newlines, + multiple-statements, + superfluous-parens, + mixed-line-endings, + unexpected-line-ending-format, + multiple-imports, + wrong-import-order, + ungrouped-imports, + wrong-import-position, + useless-import-alias, + import-outside-toplevel, + len-as-condition, + syntax-error, + unrecognized-inline-option, + bad-option-value, + init-is-generator, + return-in-init, + function-redefined, + not-in-loop, + return-outside-function, + yield-outside-function, + return-arg-in-generator, + nonexistent-operator, + duplicate-argument-name, + abstract-class-instantiated, + bad-reversed-sequence, + too-many-star-expressions, + invalid-star-assignment-target, + star-needs-assignment-target, + nonlocal-and-global, + continue-in-finally, + nonlocal-without-binding, + used-prior-global-declaration, + misplaced-format-function, + method-hidden, + access-member-before-definition, + no-method-argument, + no-self-argument, + invalid-slots-object, + assigning-non-slot, + invalid-slots, + inherit-non-class, + inconsistent-mro, + duplicate-bases, + class-variable-slots-conflict, + non-iterator-returned, + unexpected-special-method-signature, + invalid-length-returned, + invalid-bool-returned, + invalid-index-returned, + invalid-repr-returned, + invalid-str-returned, + invalid-bytes-returned, + invalid-hash-returned, + invalid-length-hint-returned, + invalid-format-returned, + invalid-getnewargs-returned, + invalid-getnewargs-ex-returned, + import-error, + relative-beyond-top-level, + used-before-assignment, + undefined-variable, + undefined-all-variable, + invalid-all-object, + no-name-in-module, + unpacking-non-sequence, + bad-except-order, + raising-bad-type, + bad-exception-context, + misplaced-bare-raise, + raising-non-exception, + notimplemented-raised, + catching-non-exception, + bad-super-call, + no-member, + not-callable, + assignment-from-no-return, + no-value-for-parameter, + too-many-function-args, + unexpected-keyword-arg, + redundant-keyword-arg, + missing-kwoa, + invalid-sequence-index, + invalid-slice-index, + assignment-from-none, + not-context-manager, + invalid-unary-operand-type, + unsupported-binary-operation, + repeated-keyword, + not-an-iterable, + not-a-mapping, + unsupported-membership-test, + unsubscriptable-object, + unsupported-assignment-operation, + unsupported-delete-operation, + invalid-metaclass, + unhashable-dict-key, + dict-iter-missing-items, + logging-unsupported-format, + logging-format-truncated, + logging-too-many-args, + logging-too-few-args, + bad-format-character, + truncated-format-string, + mixed-format-string, + format-needs-mapping, + missing-format-string-key, + too-many-format-args, + too-few-format-args, + bad-string-format-type, + bad-str-strip-call, + invalid-envvar-value, + print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + yield-inside-async-function, + not-async-context-manager, + fatal, + astroid-error, + parse-error, + method-check-failed, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + c-extension-no-member, + literal-comparison, + comparison-with-itself, + no-self-use, + no-classmethod-decorator, + no-staticmethod-decorator, + useless-object-inheritance, + property-with-parameters, + cyclic-import, + duplicate-code, + too-many-ancestors, + too-many-instance-attributes, + too-few-public-methods, + too-many-public-methods, + too-many-return-statements, + too-many-branches, + too-many-arguments, + too-many-locals, + too-many-statements, + too-many-boolean-expressions, + consider-merging-isinstance, + too-many-nested-blocks, + simplifiable-if-statement, + redefined-argument-from-local, + no-else-return, + consider-using-ternary, + trailing-comma-tuple, + stop-iteration-return, + simplify-boolean-expression, + inconsistent-return-statements, + useless-return, + consider-swap-variables, + consider-using-join, + consider-using-in, + consider-using-get, + chained-comparison, + consider-using-dict-comprehension, + consider-using-set-comprehension, + simplifiable-if-expression, + no-else-raise, + unnecessary-comprehension, + consider-using-sys-exit, + no-else-break, + no-else-continue, + super-with-arguments, + unreachable, + dangerous-default-value, + pointless-statement, + pointless-string-statement, + expression-not-assigned, + unnecessary-pass, + unnecessary-lambda, + duplicate-key, + assign-to-new-keyword, + useless-else-on-loop, + exec-used, + eval-used, + confusing-with-statement, + using-constant-test, + missing-parentheses-for-call-in-test, + self-assigning-variable, + redeclared-assigned-name, + assert-on-string-literal, + comparison-with-callable, + lost-exception, + assert-on-tuple, + attribute-defined-outside-init, + bad-staticmethod-argument, + protected-access, + arguments-differ, + signature-differs, + abstract-method, + super-init-not-called, + no-init, + non-parent-init-called, + useless-super-delegation, + invalid-overridden-method, + unnecessary-semicolon, + bad-indentation, + wildcard-import, + deprecated-module, + reimported, + import-self, + preferred-module, + misplaced-future, + fixme, + global-variable-undefined, + global-variable-not-assigned, + global-statement, + global-at-module-level, + unused-import, + unused-variable, + unused-argument, + unused-wildcard-import, + redefined-outer-name, + redefined-builtin, + redefine-in-handler, + undefined-loop-variable, + unbalanced-tuple-unpacking, + cell-var-from-loop, + possibly-unused-variable, + self-cls-assignment, + bare-except, + broad-except, + duplicate-except, + try-except-raise, + raise-missing-from, + binary-op-exception, + raising-format-tuple, + wrong-exception-operation, + keyword-arg-before-vararg, + arguments-out-of-order, + non-str-assignment-to-dunder-name, + isinstance-second-argument-not-valid-type, + logging-not-lazy, + logging-format-interpolation, + logging-fstring-interpolation, + bad-format-string-key, + unused-format-string-key, + bad-format-string, + missing-format-argument-key, + unused-format-string-argument, + format-combined-specification, + missing-format-attribute, + invalid-format-index, + duplicate-string-formatting-argument, + f-string-without-interpolation, + anomalous-backslash-in-string, + anomalous-unicode-escape-in-string, + implicit-str-concat, + inconsistent-quotes, + bad-open-mode, + boolean-datetime, + redundant-unittest-assert, + deprecated-method, + bad-thread-instantiation, + shallow-copy-environ, + invalid-envvar-default, + subprocess-popen-preexec-fn, + subprocess-run-check, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=wrong-spelling-in-comment, + wrong-spelling-in-docstring, + invalid-characters-in-docstring + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=no + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA +# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA +# (hunspell). +spelling-dict=en_US + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file=.lint/custom_dict + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception From 97731bde243a211d09431a7f51be455bae3f8e25 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 15 Mar 2021 15:30:19 -0600 Subject: [PATCH 087/151] fix some linting --- .lint/custom_dict | 21 - .lint/general_rc | 592 ------------------------ .lint/spelling_rc | 870 ------------------------------------ setup.py | 4 +- svg2mod/coloredlogger.py | 2 +- svg2mod/svg/svg/__init__.py | 4 +- svg2mod/svg/svg/geometry.py | 2 +- svg2mod/svg/svg/svg.py | 58 +-- svg2mod/svg2mod.py | 32 +- 9 files changed, 54 insertions(+), 1531 deletions(-) delete mode 100644 .lint/custom_dict delete mode 100644 .lint/general_rc delete mode 100644 .lint/spelling_rc diff --git a/.lint/custom_dict b/.lint/custom_dict deleted file mode 100644 index f8a0557..0000000 --- a/.lint/custom_dict +++ /dev/null @@ -1,21 +0,0 @@ -svg -inkscape -kicad -stderr -stdout -vect -rx -ry -json -px -bezier -txt -cjlano -sys -Ramer -Douglas -Peucker -Traceback -precompute -pdistance -elt diff --git a/.lint/general_rc b/.lint/general_rc deleted file mode 100644 index 53c2022..0000000 --- a/.lint/general_rc +++ /dev/null @@ -1,592 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10.0 - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=bare-except, - invalid-name, - no-member, - print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA -# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA -# (hunspell). -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/.lint/spelling_rc b/.lint/spelling_rc deleted file mode 100644 index 6071865..0000000 --- a/.lint/spelling_rc +++ /dev/null @@ -1,870 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10.0 - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=blacklisted-name, - invalid-name, - empty-docstring, - unneeded-not, - missing-module-docstring, - missing-class-docstring, - missing-function-docstring, - singleton-comparison, - misplaced-comparison-constant, - unidiomatic-typecheck, - non-ascii-name, - consider-using-enumerate, - consider-iterating-dictionary, - bad-classmethod-argument, - bad-mcs-method-argument, - bad-mcs-classmethod-argument, - single-string-used-for-slots, - line-too-long, - too-many-lines, - trailing-whitespace, - missing-final-newline, - trailing-newlines, - multiple-statements, - superfluous-parens, - mixed-line-endings, - unexpected-line-ending-format, - multiple-imports, - wrong-import-order, - ungrouped-imports, - wrong-import-position, - useless-import-alias, - import-outside-toplevel, - len-as-condition, - syntax-error, - unrecognized-inline-option, - bad-option-value, - init-is-generator, - return-in-init, - function-redefined, - not-in-loop, - return-outside-function, - yield-outside-function, - return-arg-in-generator, - nonexistent-operator, - duplicate-argument-name, - abstract-class-instantiated, - bad-reversed-sequence, - too-many-star-expressions, - invalid-star-assignment-target, - star-needs-assignment-target, - nonlocal-and-global, - continue-in-finally, - nonlocal-without-binding, - used-prior-global-declaration, - misplaced-format-function, - method-hidden, - access-member-before-definition, - no-method-argument, - no-self-argument, - invalid-slots-object, - assigning-non-slot, - invalid-slots, - inherit-non-class, - inconsistent-mro, - duplicate-bases, - class-variable-slots-conflict, - non-iterator-returned, - unexpected-special-method-signature, - invalid-length-returned, - invalid-bool-returned, - invalid-index-returned, - invalid-repr-returned, - invalid-str-returned, - invalid-bytes-returned, - invalid-hash-returned, - invalid-length-hint-returned, - invalid-format-returned, - invalid-getnewargs-returned, - invalid-getnewargs-ex-returned, - import-error, - relative-beyond-top-level, - used-before-assignment, - undefined-variable, - undefined-all-variable, - invalid-all-object, - no-name-in-module, - unpacking-non-sequence, - bad-except-order, - raising-bad-type, - bad-exception-context, - misplaced-bare-raise, - raising-non-exception, - notimplemented-raised, - catching-non-exception, - bad-super-call, - no-member, - not-callable, - assignment-from-no-return, - no-value-for-parameter, - too-many-function-args, - unexpected-keyword-arg, - redundant-keyword-arg, - missing-kwoa, - invalid-sequence-index, - invalid-slice-index, - assignment-from-none, - not-context-manager, - invalid-unary-operand-type, - unsupported-binary-operation, - repeated-keyword, - not-an-iterable, - not-a-mapping, - unsupported-membership-test, - unsubscriptable-object, - unsupported-assignment-operation, - unsupported-delete-operation, - invalid-metaclass, - unhashable-dict-key, - dict-iter-missing-items, - logging-unsupported-format, - logging-format-truncated, - logging-too-many-args, - logging-too-few-args, - bad-format-character, - truncated-format-string, - mixed-format-string, - format-needs-mapping, - missing-format-string-key, - too-many-format-args, - too-few-format-args, - bad-string-format-type, - bad-str-strip-call, - invalid-envvar-value, - print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - yield-inside-async-function, - not-async-context-manager, - fatal, - astroid-error, - parse-error, - method-check-failed, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - c-extension-no-member, - literal-comparison, - comparison-with-itself, - no-self-use, - no-classmethod-decorator, - no-staticmethod-decorator, - useless-object-inheritance, - property-with-parameters, - cyclic-import, - duplicate-code, - too-many-ancestors, - too-many-instance-attributes, - too-few-public-methods, - too-many-public-methods, - too-many-return-statements, - too-many-branches, - too-many-arguments, - too-many-locals, - too-many-statements, - too-many-boolean-expressions, - consider-merging-isinstance, - too-many-nested-blocks, - simplifiable-if-statement, - redefined-argument-from-local, - no-else-return, - consider-using-ternary, - trailing-comma-tuple, - stop-iteration-return, - simplify-boolean-expression, - inconsistent-return-statements, - useless-return, - consider-swap-variables, - consider-using-join, - consider-using-in, - consider-using-get, - chained-comparison, - consider-using-dict-comprehension, - consider-using-set-comprehension, - simplifiable-if-expression, - no-else-raise, - unnecessary-comprehension, - consider-using-sys-exit, - no-else-break, - no-else-continue, - super-with-arguments, - unreachable, - dangerous-default-value, - pointless-statement, - pointless-string-statement, - expression-not-assigned, - unnecessary-pass, - unnecessary-lambda, - duplicate-key, - assign-to-new-keyword, - useless-else-on-loop, - exec-used, - eval-used, - confusing-with-statement, - using-constant-test, - missing-parentheses-for-call-in-test, - self-assigning-variable, - redeclared-assigned-name, - assert-on-string-literal, - comparison-with-callable, - lost-exception, - assert-on-tuple, - attribute-defined-outside-init, - bad-staticmethod-argument, - protected-access, - arguments-differ, - signature-differs, - abstract-method, - super-init-not-called, - no-init, - non-parent-init-called, - useless-super-delegation, - invalid-overridden-method, - unnecessary-semicolon, - bad-indentation, - wildcard-import, - deprecated-module, - reimported, - import-self, - preferred-module, - misplaced-future, - fixme, - global-variable-undefined, - global-variable-not-assigned, - global-statement, - global-at-module-level, - unused-import, - unused-variable, - unused-argument, - unused-wildcard-import, - redefined-outer-name, - redefined-builtin, - redefine-in-handler, - undefined-loop-variable, - unbalanced-tuple-unpacking, - cell-var-from-loop, - possibly-unused-variable, - self-cls-assignment, - bare-except, - broad-except, - duplicate-except, - try-except-raise, - raise-missing-from, - binary-op-exception, - raising-format-tuple, - wrong-exception-operation, - keyword-arg-before-vararg, - arguments-out-of-order, - non-str-assignment-to-dunder-name, - isinstance-second-argument-not-valid-type, - logging-not-lazy, - logging-format-interpolation, - logging-fstring-interpolation, - bad-format-string-key, - unused-format-string-key, - bad-format-string, - missing-format-argument-key, - unused-format-string-argument, - format-combined-specification, - missing-format-attribute, - invalid-format-index, - duplicate-string-formatting-argument, - f-string-without-interpolation, - anomalous-backslash-in-string, - anomalous-unicode-escape-in-string, - implicit-str-concat, - inconsistent-quotes, - bad-open-mode, - boolean-datetime, - redundant-unittest-assert, - deprecated-method, - bad-thread-instantiation, - shallow-copy-environ, - invalid-envvar-default, - subprocess-popen-preexec-fn, - subprocess-run-check, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=wrong-spelling-in-comment, - wrong-spelling-in-docstring, - invalid-characters-in-docstring - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA -# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA -# (hunspell). -spelling-dict=en_US - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file=.lint/custom_dict - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/setup.py b/setup.py index 1e11b6e..104c7bc 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ tag = subprocess.check_output(["git","describe","--tag"], stderr=subprocess.STDOUT) tag = tag.stdout.decode('utf-8') tag = tag.replace("-", ".dev", 1).replace("-", "+") -except (FileNotFoundError, subprocess.CalledProcessError) as e: +except: tag = "0.dev0" requirements = [ @@ -28,7 +28,7 @@ ] test_requirements = [ - # TODO: put package test requirements here + "pytest", "pylint", "pyenchant" ] setup( diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index 9bd8f5a..c125f2f 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -38,4 +38,4 @@ def split_logger(logger, formatter=Formatter(), breakpoint=logging.WARNING): logger.addHandler(hdlrerr) logger.addHandler(hdlrout) - + diff --git a/svg2mod/svg/svg/__init__.py b/svg2mod/svg/svg/__init__.py index 3090eb7..45553d2 100644 --- a/svg2mod/svg/svg/__init__.py +++ b/svg2mod/svg/svg/__init__.py @@ -3,6 +3,4 @@ from .svg import * def parse(filename): - f = svg.Svg(filename) - return f - + return Svg(filename) diff --git a/svg2mod/svg/svg/geometry.py b/svg2mod/svg/svg/geometry.py index d4f1f32..f036eb9 100644 --- a/svg2mod/svg/svg/geometry.py +++ b/svg2mod/svg/svg/geometry.py @@ -252,7 +252,7 @@ def rbbox(self): def segments(self, precision=0): '''Return a poly-line approximation ("segments") of the Bezier curve - precision is the minimum significative length of a segment''' + precision is the minimum significant length of a segment''' segments = [] # n is the number of Bezier points to draw according to precision if precision != 0: diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 6876872..36a2754 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -1,7 +1,7 @@ # SVG parser in Python # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > -# Copyright (C) 2021 -- svg2mod developers < github.com / svg2mod > +# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -77,7 +77,7 @@ def __init__(self, elt=None): self.viewport = Point(800, 600) # default viewport is 800x600 if elt is not None: self.id = elt.get('id', self.id) - # Parse transform attibute to update self.matrix + # Parse transform attribute to update self.matrix self.getTransformations(elt) def bbox(self): @@ -100,8 +100,8 @@ def getTransformations(self, elt): svg_transforms = [ 'matrix', 'translate', 'scale', 'rotate', 'skewX', 'skewY'] - # match any SVG transformation with its parameter (until final parenthese) - # [^)]* == anything but a closing parenthese + # match any SVG transformation with its parameter (until final parenthesis) + # [^)]* == anything but a closing parenthesis # '|'.join == OR-list of SVG transformations transforms = re.findall( '|'.join([x + '[^)]*\)' for x in svg_transforms]), t) @@ -237,7 +237,7 @@ def parse(self, filename): if self.root.get('viewBox') is not None: viewBox = re.findall(number_re, self.root.get('viewBox')) - # If the document somehow doesn't have dimentions get if from viewBox + # If the document somehow doesn't have dimensions get if from viewBox if self.root.get('width') is None or self.root.get('height') is None: width = float(viewBox[2]) height = float(viewBox[3]) @@ -304,7 +304,7 @@ def append(self, element): if elt_class is None: logging.debug('No handler for element %s' % elt.tag) continue - # instanciate elt associated class (e.g. : item = Path(elt) + # instantiate elt associated class (e.g. : item = Path(elt) item = elt_class(elt) # Apply group matrix to the newly created object # Actually, this is effectively done in Svg.__init__() through call to @@ -333,8 +333,10 @@ class Matrix: (0, 0, 1)) see http://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace ''' - def __init__(self, vect=[1, 0, 0, 1, 0, 0]): + def __init__(self, vect=None): # Unit transformation vect by default + if vect is None: + vect = [1, 0, 0, 1, 0, 0] if len(vect) != 6: raise ValueError("Bad vect size %d" % len(vect)) self.vect = list(vect) @@ -591,7 +593,7 @@ def transform(self, matrix=None): def P(self, t): '''Return a Point on the Ellipse for t in [0..1]''' - #TODO change point cords if rotaion + #TODO change point cords if rotation is set x = self.center.x + self.rx * math.cos(2 * math.pi * t) y = self.center.y + self.ry * math.sin(2 * math.pi * t) return Point(x,y) @@ -619,7 +621,7 @@ def segments(self, precision=0): def simplify(self, precision): return self -# An arc is an ellipse with a beginning and an end point instead of an entire circumference +# An arc is an ellipse with a beginning and an end point instead of an entire circumference class Arc(Ellipse): '''SVG ''' # class Ellipse handles the tag @@ -642,7 +644,7 @@ def __init__(self, start_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt): def __repr__(self): return '' - + def calcuate_center(self): angle = Angle(math.radians(self.rotation)) @@ -651,8 +653,8 @@ def calcuate_center(self): cs2 = 2*angle.cos*angle.sin*(math.pow(self.ry, 2) - math.pow(self.rx, 2)) rs = (math.pow(self.ry*angle.sin, 2) + math.pow(self.rx*angle.cos, 2)) rc = (math.pow(self.ry*angle.cos, 2) + math.pow(self.rx*angle.sin, 2)) - - + + # Create a line that passes through both intersection points y = -pts[0].x*(cs2) + pts[1].x*cs2 - 2*pts[0].y*rs + 2*pts[1].y*rs # Round to prevent floating point errors @@ -662,29 +664,29 @@ def calcuate_center(self): # Finish calculating the line m = ( -2*pts[0].x*rc + 2*pts[1].x*rc - pts[0].y*cs2 + pts[1].y*cs2 ) / -y b = ( math.pow(pts[0].x,2)*rc - math.pow(pts[1].x,2)*rc + pts[0].x*pts[0].y*cs2 - pts[1].x*pts[1].y*cs2 + math.pow(pts[0].y,2)*(rs) - math.pow(pts[1].y,2)*rs ) / -y - + # Now that we have a line we can setup a quadratic equation to solve for all intersection points qa = rc + m*cs2 + math.pow(m,2)*rs qb = -2*pts[0].x*rc + b*cs2 - pts[0].y*cs2 - m*pts[0].x*cs2 + 2*m*b*rs - 2*pts[0].y*m*rs qc = math.pow(pts[0].x,2)*rc - b*pts[0].x*cs2 + pts[0].x*pts[0].y*cs2 + math.pow(b,2)*rs - 2*b*pts[0].y*rs + math.pow(pts[0].y,2)*rs - math.pow(self.rx*self.ry, 2) - + else: # When the slope is vertical we need to calculate with x instead of y x = (pts[0].x+pts[1].x)/2 m=0 b=x - + # The quadratic formula but solving for y instead of x and only when the slope is vertical qa = rs qb = x*cs2 - pts[0].x*cs2 - 2*pts[0].y*rs qc = math.pow(x,2)*rc - 2*x*pts[0].x*rc + math.pow(pts[0].x,2)*rc - x*pts[0].y*cs2 + pts[0].x*pts[0].y*cs2 + math.pow(pts[0].y,2)*rs - math.pow(self.rx*self.ry, 2) - - # This is the value to see how many real solutions the quadratic equation has. + + # This is the value to see how many real solutions the quadratic equation has. # if root is negative then there are only imaginary solutions or no real solutions # if the root is 0 then there is one solution # otherwise there are two solutions root = math.pow(qb, 2) - 4*qa*qc - + # If there are no roots then we need to scale the arc to fit the points if root < 0: # Center point @@ -732,7 +734,7 @@ def calcuate_center(self): point = points[target if not self.large_arc_flag else target ^ 1 ] - + # Swap the x and y results from when the intersection line is vertical because we solved for y instead of x # Also remove any insignificant floating point errors if y == 0: @@ -760,7 +762,7 @@ def segments(self, precision=0): if max(self.rx, self.ry) < precision: return self.end_pts return Ellipse.segments(self, precision)[0] - + def P(self, t): '''Return a Point on the Arc for t in [0..1]''' #TODO change point cords if rotation is set @@ -920,13 +922,13 @@ def __init__(self, elt=None, parent=None): if self.font_family: self.font_file = self.find_font_file() self.text = [] - + def set_font(self, font=None, bold=None, italic=None, size=None): font = font if font else self.font_family bold = bold if bold else (self.bold.lower() != "normal") italic = italic if italic else (self.italic.lower() != "normal") size = size if size else self.size - if type(size) is str: + if isinstance(size, str): size = float(size.strip("px")) self.font_family = font @@ -970,8 +972,8 @@ def parse(self, elt, parent): value = nv[ 1 ].strip() if list(self.font_configs.keys()).count(name) != 0: self.font_configs[name] = value - - if type(self.font_configs["font-size"]) is str: + + if isinstance(self.font_configs["font-size"], str): float(self.font_configs["font-size"].strip("px")) for config in self.font_configs: @@ -1027,7 +1029,7 @@ def find_font_file(self): )) self.paths = [] return - + bold = self.bold is not None and self.bold.lower() != "normal" italic = self.italic is not None and self.italic.lower() != "normal" @@ -1122,7 +1124,7 @@ def convert_to_path(self, auto_transform=True): self.paths.append(path) if auto_transform: self.transform() - + def bbox(self): if self.paths is None or len(self.paths) == 0: return [Point(0,0),Point(0,0)] @@ -1133,7 +1135,7 @@ def bbox(self): Point(min(bboxes, key=lambda v: v[0].x)[0].x, min(bboxes, key=lambda v: v[0].y)[0].y), Point(max(bboxes, key=lambda v: v[1].x)[1].x, max(bboxes, key=lambda v: v[1].y)[1].y), ] - + def transform(self, matrix=None): if matrix is None: matrix = self.matrix @@ -1143,7 +1145,7 @@ def transform(self, matrix=None): for paths in self.paths: for path in paths: path.transform(matrix) - + def segments(self, precision=0): segs = [] for paths in self.paths: diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 343c429..957b172 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1,7 +1,13 @@ -import argparse, datetime -import shlex, os, sys, re -import logging, io, time +import argparse +import datetime +import shlex +import os +import sys +import re +import io +import time +import logging import svg2mod.svg as svg import svg2mod.coloredlogger as coloredlogger @@ -274,7 +280,7 @@ def _find_insertion_point( self, hole, holes, other_insertions ): connected = [self] + list( connected[2] ) else: connected = [self] - + for hp in range( len(hole.points) - 1 ): for poly in connected: for pp in range( len(poly.points ) - 1 ): @@ -505,14 +511,14 @@ def are_distinct(self, polygon): larger = self if smaller == polygon else polygon if ( - larger.bbox[0].x < smaller.bbox[0].x and + larger.bbox[0].x < smaller.bbox[0].x and larger.bbox[0].y < smaller.bbox[0].y and larger.bbox[1].x > smaller.bbox[1].x and - larger.bbox[1].y > smaller.bbox[1].y + larger.bbox[1].y > smaller.bbox[1].y ): distinct = False - # Check number of horizontal intersections. If the number is odd then it the smaller polygon + # Check number of horizontal intersections. If the number is odd then it the smaller polygon # is contained. If the number is even then the polygon is outside of the larger polygon if not distinct: tline = LineSegment(smaller.points[0], svg.Point(larger.bbox[1].x, smaller.points[0].y)) @@ -617,7 +623,7 @@ def _get_fill_stroke( self, item ): # the top-level viewport_scale scale = self.imported.svg.viewport_scale / self.scale_factor - # remove unnecessary precession to reduce floating point errors + # remove unnecessary precision to reduce floating point errors stroke_width = round(stroke_width/scale, 6) elif name == "stroke-opacity": @@ -782,7 +788,7 @@ def _write_items( self, items, layer, flip = False ): points = inlinable[ 0 ].points logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) - + self._write_polygon( points, layer, fill, stroke, stroke_width ) @@ -794,7 +800,7 @@ def _write_items( self, items, layer, flip = False ): if len ( segments ) == 1: logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) - + self._write_polygon( points, layer, fill, stroke, stroke_width ) @@ -1118,9 +1124,9 @@ def _write_polygon_header( self, points, layer ): def _write_polygon_point( self, point ): - self.output_file.write( - "Dl {} {}\n".format( point.x, point.y ) - ) + self.output_file.write( + "Dl {} {}\n".format( point.x, point.y ) + ) #------------------------------------------------------------------------ From 34397ca6fb84d0b6bab73367bfe005c8e57f3d09 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 20 Mar 2021 00:56:25 -0600 Subject: [PATCH 088/151] Document code and work on adding linting --- .gitignore | 6 +- .linter/{cleanup_rc => cleanup.rc} | 5 +- .linter/custom_dict | 10 + .linter/{general_rc => required-linting.rc} | 11 +- .linter/spelling_rc | 870 -------------------- .travis.yml | 4 +- setup.cfg | 5 + setup.py | 12 +- svg2mod/coloredlogger.py | 27 +- svg2mod/svg/__init__.py | 5 + svg2mod/svg/svg/__init__.py | 5 + svg2mod/svg/svg/geometry.py | 12 + svg2mod/svg/svg/svg.py | 339 ++++++-- svg2mod/svg2mod.py | 35 +- 14 files changed, 361 insertions(+), 985 deletions(-) rename .linter/{cleanup_rc => cleanup.rc} (99%) rename .linter/{general_rc => required-linting.rc} (98%) delete mode 100644 .linter/spelling_rc create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index 5becbb9..030b949 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ build dist svg2mod.egg-info +.eggs *.kicad_mod *.mod -__pycache__ \ No newline at end of file +__pycache__ +.pytest-cache +Pipfile +Pipfile.lock diff --git a/.linter/cleanup_rc b/.linter/cleanup.rc similarity index 99% rename from .linter/cleanup_rc rename to .linter/cleanup.rc index 2daa034..379662d 100644 --- a/.linter/cleanup_rc +++ b/.linter/cleanup.rc @@ -99,6 +99,7 @@ enable=c-extension-no-member, unused-variable, unused-argument, unused-wildcard-import, + attribute-defined-outside-init, [REPORTS] @@ -344,10 +345,10 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=150 # Maximum number of lines in a module. -max-module-lines=1000 +max-module-lines=1500 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. diff --git a/.linter/custom_dict b/.linter/custom_dict index c8110a4..9721dda 100644 --- a/.linter/custom_dict +++ b/.linter/custom_dict @@ -26,3 +26,13 @@ attrib viewport init collinear +un +formatter +pre +inlined +otf +ttf +xml +iterable +decompiles +py diff --git a/.linter/general_rc b/.linter/required-linting.rc similarity index 98% rename from .linter/general_rc rename to .linter/required-linting.rc index 53d08cb..a1bc3eb 100644 --- a/.linter/general_rc +++ b/.linter/required-linting.rc @@ -22,7 +22,7 @@ ignore-patterns= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the # number of processors available to use. -jobs=0 +jobs=1 # Control the amount of potential inferred values when inferring a single # object. This can help the performance when dealing with large functions or @@ -75,7 +75,7 @@ enable=c-extension-no-member, bad-indentation, unidiomatic-typecheck, undefined-variable, - no-member, + #no-member, dangerous-default-value, expression-not-assigned, invalid-length-returned, @@ -112,6 +112,9 @@ enable=c-extension-no-member, assignment-from-no-return, fatal, parse-error, + wrong-spelling-in-comment, + wrong-spelling-in-docstring, + invalid-characters-in-docstring [REPORTS] @@ -189,13 +192,13 @@ max-spelling-suggestions=4 # Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA # (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA # (hunspell). -spelling-dict= +spelling-dict=en_US # List of comma separated words that should not be checked. spelling-ignore-words= # A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= +spelling-private-dict-file=.linter/custom_dict # Tells whether to store unknown words to the private dictionary (see the # --spelling-private-dict-file option) instead of raising a message. diff --git a/.linter/spelling_rc b/.linter/spelling_rc deleted file mode 100644 index a58d56a..0000000 --- a/.linter/spelling_rc +++ /dev/null @@ -1,870 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10.0 - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=blacklisted-name, - invalid-name, - empty-docstring, - unneeded-not, - missing-module-docstring, - missing-class-docstring, - missing-function-docstring, - singleton-comparison, - misplaced-comparison-constant, - unidiomatic-typecheck, - non-ascii-name, - consider-using-enumerate, - consider-iterating-dictionary, - bad-classmethod-argument, - bad-mcs-method-argument, - bad-mcs-classmethod-argument, - single-string-used-for-slots, - line-too-long, - too-many-lines, - trailing-whitespace, - missing-final-newline, - trailing-newlines, - multiple-statements, - superfluous-parens, - mixed-line-endings, - unexpected-line-ending-format, - multiple-imports, - wrong-import-order, - ungrouped-imports, - wrong-import-position, - useless-import-alias, - import-outside-toplevel, - len-as-condition, - syntax-error, - unrecognized-inline-option, - bad-option-value, - init-is-generator, - return-in-init, - function-redefined, - not-in-loop, - return-outside-function, - yield-outside-function, - return-arg-in-generator, - nonexistent-operator, - duplicate-argument-name, - abstract-class-instantiated, - bad-reversed-sequence, - too-many-star-expressions, - invalid-star-assignment-target, - star-needs-assignment-target, - nonlocal-and-global, - continue-in-finally, - nonlocal-without-binding, - used-prior-global-declaration, - misplaced-format-function, - method-hidden, - access-member-before-definition, - no-method-argument, - no-self-argument, - invalid-slots-object, - assigning-non-slot, - invalid-slots, - inherit-non-class, - inconsistent-mro, - duplicate-bases, - class-variable-slots-conflict, - non-iterator-returned, - unexpected-special-method-signature, - invalid-length-returned, - invalid-bool-returned, - invalid-index-returned, - invalid-repr-returned, - invalid-str-returned, - invalid-bytes-returned, - invalid-hash-returned, - invalid-length-hint-returned, - invalid-format-returned, - invalid-getnewargs-returned, - invalid-getnewargs-ex-returned, - import-error, - relative-beyond-top-level, - used-before-assignment, - undefined-variable, - undefined-all-variable, - invalid-all-object, - no-name-in-module, - unpacking-non-sequence, - bad-except-order, - raising-bad-type, - bad-exception-context, - misplaced-bare-raise, - raising-non-exception, - notimplemented-raised, - catching-non-exception, - bad-super-call, - no-member, - not-callable, - assignment-from-no-return, - no-value-for-parameter, - too-many-function-args, - unexpected-keyword-arg, - redundant-keyword-arg, - missing-kwoa, - invalid-sequence-index, - invalid-slice-index, - assignment-from-none, - not-context-manager, - invalid-unary-operand-type, - unsupported-binary-operation, - repeated-keyword, - not-an-iterable, - not-a-mapping, - unsupported-membership-test, - unsubscriptable-object, - unsupported-assignment-operation, - unsupported-delete-operation, - invalid-metaclass, - unhashable-dict-key, - dict-iter-missing-items, - logging-unsupported-format, - logging-format-truncated, - logging-too-many-args, - logging-too-few-args, - bad-format-character, - truncated-format-string, - mixed-format-string, - format-needs-mapping, - missing-format-string-key, - too-many-format-args, - too-few-format-args, - bad-string-format-type, - bad-str-strip-call, - invalid-envvar-value, - print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - yield-inside-async-function, - not-async-context-manager, - fatal, - astroid-error, - parse-error, - method-check-failed, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - c-extension-no-member, - literal-comparison, - comparison-with-itself, - no-self-use, - no-classmethod-decorator, - no-staticmethod-decorator, - useless-object-inheritance, - property-with-parameters, - cyclic-import, - duplicate-code, - too-many-ancestors, - too-many-instance-attributes, - too-few-public-methods, - too-many-public-methods, - too-many-return-statements, - too-many-branches, - too-many-arguments, - too-many-locals, - too-many-statements, - too-many-boolean-expressions, - consider-merging-isinstance, - too-many-nested-blocks, - simplifiable-if-statement, - redefined-argument-from-local, - no-else-return, - consider-using-ternary, - trailing-comma-tuple, - stop-iteration-return, - simplify-boolean-expression, - inconsistent-return-statements, - useless-return, - consider-swap-variables, - consider-using-join, - consider-using-in, - consider-using-get, - chained-comparison, - consider-using-dict-comprehension, - consider-using-set-comprehension, - simplifiable-if-expression, - no-else-raise, - unnecessary-comprehension, - consider-using-sys-exit, - no-else-break, - no-else-continue, - super-with-arguments, - unreachable, - dangerous-default-value, - pointless-statement, - pointless-string-statement, - expression-not-assigned, - unnecessary-pass, - unnecessary-lambda, - duplicate-key, - assign-to-new-keyword, - useless-else-on-loop, - exec-used, - eval-used, - confusing-with-statement, - using-constant-test, - missing-parentheses-for-call-in-test, - self-assigning-variable, - redeclared-assigned-name, - assert-on-string-literal, - comparison-with-callable, - lost-exception, - assert-on-tuple, - attribute-defined-outside-init, - bad-staticmethod-argument, - protected-access, - arguments-differ, - signature-differs, - abstract-method, - super-init-not-called, - no-init, - non-parent-init-called, - useless-super-delegation, - invalid-overridden-method, - unnecessary-semicolon, - bad-indentation, - wildcard-import, - deprecated-module, - reimported, - import-self, - preferred-module, - misplaced-future, - fixme, - global-variable-undefined, - global-variable-not-assigned, - global-statement, - global-at-module-level, - unused-import, - unused-variable, - unused-argument, - unused-wildcard-import, - redefined-outer-name, - redefined-builtin, - redefine-in-handler, - undefined-loop-variable, - unbalanced-tuple-unpacking, - cell-var-from-loop, - possibly-unused-variable, - self-cls-assignment, - bare-except, - broad-except, - duplicate-except, - try-except-raise, - raise-missing-from, - binary-op-exception, - raising-format-tuple, - wrong-exception-operation, - keyword-arg-before-vararg, - arguments-out-of-order, - non-str-assignment-to-dunder-name, - isinstance-second-argument-not-valid-type, - logging-not-lazy, - logging-format-interpolation, - logging-fstring-interpolation, - bad-format-string-key, - unused-format-string-key, - bad-format-string, - missing-format-argument-key, - unused-format-string-argument, - format-combined-specification, - missing-format-attribute, - invalid-format-index, - duplicate-string-formatting-argument, - f-string-without-interpolation, - anomalous-backslash-in-string, - anomalous-unicode-escape-in-string, - implicit-str-concat, - inconsistent-quotes, - bad-open-mode, - boolean-datetime, - redundant-unittest-assert, - deprecated-method, - bad-thread-instantiation, - shallow-copy-environ, - invalid-envvar-default, - subprocess-popen-preexec-fn, - subprocess-run-check, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=wrong-spelling-in-comment, - wrong-spelling-in-docstring, - invalid-characters-in-docstring - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=no - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: en_GB (hunspell), en_ZA -# (hunspell), en_AU (hunspell), en_US (hunspell), en (aspell), en_CA -# (hunspell). -spelling-dict=en_US - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file=.lint/custom_dict - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=i, - j, - k, - ex, - Run, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=6 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/.travis.yml b/.travis.yml index 4181e6e..e2351b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,9 @@ jobs: os: linux language: python script: - - pip3 install ./ + - pip install ./ pylint pyenchant + - pylint --rcfile .linter/cleanup.rc svg2mod setup.py + - python setup.py test - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug - svg2mod -i examples/svg2mod.svg --debug - svg2mod -i examples/svg2mod.svg --format legacy diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ad84911 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[aliases] +test=pytest + +[tool:pytest] +addopts = --pylint --pylint-rcfile=.linter/required-linting.rc diff --git a/setup.py b/setup.py index 104c7bc..353bf3e 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +''' +to install system wide use `pip install .` +testing is done via `python setup.py test` +''' import subprocess import setuptools @@ -27,8 +31,11 @@ "fonttools" ] +setup_requirements = [ + "pytest-runner", "pytest-pylint", +] test_requirements = [ - "pytest", "pylint", "pyenchant" + "pytest", "pylint", "pyenchant", ] setup( @@ -58,6 +65,7 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', ], - test_suite='tests', + setup_requires=setup_requirements, + test_suite='test', tests_require=test_requirements ) diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index c125f2f..ca7d4bd 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -1,11 +1,16 @@ -# A simple modification to the formatter class in the python logger to allow -# ANSI color codes based on the logged message's level +''' +A simple modification to the formatter class in the python logger to allow +ANSI color codes based on the logged message's level +''' import sys import logging class Formatter(logging.Formatter): + '''Extend formatter to add colored output functionality ''' + + # ASCII escape codes for supporting terminals color = { logging.CRITICAL: "\033[91m\033[7m", #Set red and swap background and foreground logging.ERROR: "\033[91m", #Set red @@ -14,24 +19,32 @@ class Formatter(logging.Formatter): logging.INFO: "" #Do nothing } reset = "\033[0m" # Reset the terminal back to default color/emphasis + def __init__(self, fmt="%(message)s", datefmt=None, style="%"): super().__init__(fmt, datefmt, style) def format(self, record): + '''Overwrite the format function. + This saves the original style, overwrites it to support + color, sends the message to the super.format, and + finally returns the style to the original format + ''' fmt_org = self._style._fmt self._style._fmt = Formatter.color[record.levelno] + fmt_org + Formatter.reset result = logging.Formatter.format(self, record) self._style._fmt = fmt_org return result -# This will split logging messages at the specified break point. Anything higher -# will be sent to sys.stderr and everything else to sys.stdout -def split_logger(logger, formatter=Formatter(), breakpoint=logging.WARNING): +def split_logger(logger, formatter=Formatter(), brkpoint=logging.WARNING): + '''This will split logging messages at the specified break point. Anything higher + will be sent to sys.stderr and everything else to sys.stdout + ''' + hdlrerr = logging.StreamHandler(sys.stderr) - hdlrerr.addFilter(lambda msg: breakpoint <= msg.levelno) + hdlrerr.addFilter(lambda msg: brkpoint <= msg.levelno) hdlrout = logging.StreamHandler(sys.stdout) - hdlrout.addFilter(lambda msg: breakpoint > msg.levelno) + hdlrout.addFilter(lambda msg: brkpoint > msg.levelno) hdlrerr.setFormatter(formatter) hdlrout.setFormatter(formatter) diff --git a/svg2mod/svg/__init__.py b/svg2mod/svg/__init__.py index b3c8618..53afb49 100644 --- a/svg2mod/svg/__init__.py +++ b/svg2mod/svg/__init__.py @@ -1 +1,6 @@ +''' +A SVG parser with tools to convert an XML svg file +to objects that can be simplified into points. +''' + from .svg import * diff --git a/svg2mod/svg/svg/__init__.py b/svg2mod/svg/svg/__init__.py index 45553d2..6d6bf0c 100644 --- a/svg2mod/svg/svg/__init__.py +++ b/svg2mod/svg/svg/__init__.py @@ -1,6 +1,11 @@ +''' +A SVG parser with tools to convert an XML svg file +to objects that can be simplified into points. +''' #__all__ = ['geometry', 'svg'] from .svg import * def parse(filename): + '''Take in a filename and return a SVG object of parsed file''' return Svg(filename) diff --git a/svg2mod/svg/svg/geometry.py b/svg2mod/svg/svg/geometry.py index f036eb9..0211c8d 100644 --- a/svg2mod/svg/svg/geometry.py +++ b/svg2mod/svg/svg/geometry.py @@ -25,6 +25,7 @@ import operator class Point: + '''Define a point as two floats accessible by x and y''' def __init__(self, x=None, y=None): '''A Point is defined either by a tuple/list of length 2 or by 2 coordinates @@ -195,6 +196,7 @@ def pdistance(self, p): def bbox(self): + '''Return bounding box as ( Point(min), Point(max )''' xmin = min(self.start.x, self.end.x) xmax = max(self.start.x, self.end.x) ymin = min(self.start.y, self.end.y) @@ -203,6 +205,7 @@ def bbox(self): return (Point(xmin,ymin),Point(xmax,ymax)) def transform(self, matrix): + '''Transform start and end point by provided matrix''' self.start = matrix * self.start self.end = matrix * self.end @@ -221,6 +224,7 @@ def __str__(self): ' : ' + ", ".join([str(x) for x in self.pts]) def control_point(self, n): + '''Return Point at index n''' if n >= self.dimension: raise LookupError('Index is larger than Bezier curve dimension') else: @@ -238,6 +242,7 @@ def rlength(self): return l def bbox(self): + '''This returns the rough bounding box ''' return self.rbbox() def rbbox(self): @@ -289,16 +294,23 @@ def _bezierN(self, t): return res[0] def transform(self, matrix): + '''Transform every point by the provided matrix''' self.pts = [matrix * x for x in self.pts] class MoveTo: + '''MoveTo class + This will create a move without creating a segment + to the destination point. + ''' def __init__(self, dest): self.dest = dest def bbox(self): + '''This returns a single point bounding box. ( Point(destination), Point(destination) )''' return (self.dest, self.dest) def transform(self, matrix): + '''Transform the destination point by provided matrix''' self.dest = matrix * self.dest diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg/svg.py index 36a2754..77741ea 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg/svg.py @@ -16,8 +16,11 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +''' +A SVG parser with tools to convert an XML svg file +to objects that can be simplified into points. +''' -from __future__ import absolute_import import xml.etree.ElementTree as etree import math import sys @@ -36,10 +39,7 @@ from fontTools.pens.basePen import decomposeQuadraticSegment from fontTools.misc import loggingTools -from .geometry import * - -# Make fontTools more quiet -loggingTools.configLogger(level=logging.INFO) +from .geometry import Point,Angle,Segment,Bezier,MoveTo,simplify_segment svg_ns = '{http://www.w3.org/2000/svg}' @@ -78,10 +78,10 @@ def __init__(self, elt=None): if elt is not None: self.id = elt.get('id', self.id) # Parse transform attribute to update self.matrix - self.getTransformations(elt) + self.get_transformations(elt) def bbox(self): - '''Bounding box''' + '''Bounding box of all points''' bboxes = [x.bbox() for x in self.items] if len( bboxes ) < 1: return (Point(0, 0), Point(0, 0)) @@ -93,7 +93,10 @@ def bbox(self): return (Point(xmin,ymin), Point(xmax,ymax)) # Parse transform field - def getTransformations(self, elt): + def get_transformations(self, elt): + '''Take an xml element and parse transformation commands + then apply the matrix and set any needed variables + ''' t = elt.get('transform') if t is None: return @@ -104,7 +107,7 @@ def getTransformations(self, elt): # [^)]* == anything but a closing parenthesis # '|'.join == OR-list of SVG transformations transforms = re.findall( - '|'.join([x + '[^)]*\)' for x in svg_transforms]), t) + '|'.join([x + r'[^)]*\)' for x in svg_transforms]), t) for t in transforms: op, arg = t.split('(') @@ -150,6 +153,10 @@ def getTransformations(self, elt): self.matrix *= Matrix([1, tana, 0, 1, 0, 0]) def transform(self, matrix=None): + '''Apply the provided matrix. Default (None) + If no matrix is supplied then recursively apply + it's already existing matrix to all items. + ''' if matrix is None: matrix = self.matrix else: @@ -161,6 +168,7 @@ def transform(self, matrix=None): x.transform(matrix) def length(self, v, mode='xy'): + '''Return generic 2 dimensional length of svg element''' # Handle empty (non-existing) length element if v is None: return 0 @@ -186,8 +194,10 @@ def length(self, v, mode='xy'): return float(value) * unit_convert[unit] def xlength(self, x): + '''Length of element's x component''' return self.length(x, 'x') def ylength(self, y): + '''Length of element's y component''' return self.length(y, 'y') def flatten(self): @@ -215,7 +225,10 @@ def __init__(self, filename=None): if filename: self.parse(filename) - def parse(self, filename): + def parse(self, filename:str): + '''Read provided svg xml file and + append all svg element to items list + ''' self.filename = filename tree = etree.parse(filename) self.root = tree.getroot() @@ -235,19 +248,19 @@ def parse(self, filename): # viewBox if self.root.get('viewBox') is not None: - viewBox = re.findall(number_re, self.root.get('viewBox')) + view_box = re.findall(number_re, self.root.get('viewBox')) # If the document somehow doesn't have dimensions get if from viewBox if self.root.get('width') is None or self.root.get('height') is None: - width = float(viewBox[2]) - height = float(viewBox[3]) - logging.warning("Unable to find width of height properties. Falling back to viewBox.") - - sx = width / float(viewBox[2]) - sy = height / float(viewBox[3]) - tx = -float(viewBox[0]) - ty = -float(viewBox[1]) - self.viewport_scale = round(float(viewBox[2])/width, 6) + width = float(view_box[2]) + height = float(view_box[3]) + logging.warning("Unable to find width or height properties. Using viewBox.") + + sx = width / float(view_box[2]) + sy = height / float(view_box[3]) + tx = -float(view_box[0]) + ty = -float(view_box[1]) + self.viewport_scale = round(float(view_box[2])/width, 6) top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) if ( self.root.get("width") is None or self.root.get("height") is None ) \ and self.root.get("viewBox") is None: @@ -260,6 +273,7 @@ def parse(self, filename): self.transform() def title(self): + '''Returns svg title if exists. Otherwise try to return filename''' t = self.root.find(svg_ns + 'title') if t is not None: return t @@ -267,11 +281,16 @@ def title(self): return os.path.splitext(os.path.basename(self.filename))[0] def json(self): + '''Return a dictionary of children items''' return self.items class Group(Transformable): - '''Handle svg elements''' + '''Handle svg elements + The name and hidden attributes are stored in self.name + and self.hidden respectively. These can be manually set + if object is not initialized with an xml element. + ''' # class Group handles the tag tag = 'g' @@ -281,17 +300,18 @@ def __init__(self, elt=None): self.name = "" self.hidden = False if elt is not None: - for id, value in elt.attrib.items(): + for ident, value in elt.attrib.items(): - id = self.parse_name( id ) - if id[ "name" ] == "label": + ident = self.parse_name( ident ) + if ident[ "name" ] == "label": self.name = value - if id[ "name" ] == "style": - if re.search( "display\s*:\s*none", value ): + if ident[ "name" ] == "style": + if re.search( r"display\s*:\s*none", value ): self.hidden = True @staticmethod def parse_name( tag ): + '''Read and return name from xml data''' m = re.match( r'({(.+)})?(.+)', tag ) return { 'namespace' : m.group( 2 ), @@ -299,6 +319,12 @@ def parse_name( tag ): } def append(self, element): + '''Convert and append xml element(s) to items list + element is expected to be iterable. + If an svg non xml object needs to be appended + then interface directly with the items list: + group.items.append(svg_object) + ''' for elt in element: elt_class = svgClass.get(elt.tag, None) if elt_class is None: @@ -322,6 +348,7 @@ def __repr__(self): return ': ' + repr(self.items) def json(self): + '''Return json formatted dictionary of group''' return {'Group ' + self.id + " ({})".format( self.name ) : self.items} class Matrix: @@ -366,17 +393,23 @@ def __str__(self): return str(self.vect) def xlength(self, x): + '''x scale of vector''' return x * self.vect[0] def ylength(self, y): + '''y scale of vector''' return y * self.vect[3] -COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' class Path(Transformable): - '''SVG ''' + '''SVG tag handler + self.items contains all objects for path instructions. + Calling .parse(...) will append new path instruction + objects to items list. + ''' # class Path handles the tag tag = 'path' + COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' def __init__(self, elt=None): Transformable.__init__(self, elt) @@ -384,10 +417,10 @@ def __init__(self, elt=None): self.style = elt.get('style') self.parse(elt.get('d')) - def parse(self, pathstr): - """Parse path string and build elements list""" + def parse(self, pathstr:str): + """Parse svg path string and build elements list""" - pathlst = re.findall(number_re + r"|\ *[%s]\ *" % COMMANDS, pathstr) + pathlst = re.findall(number_re + r"|\ *[%s]\ *" % Path.COMMANDS, pathstr) pathlst.reverse() @@ -396,7 +429,7 @@ def parse(self, pathstr): start_pt = None while pathlst: - if pathlst[-1].strip() in COMMANDS: + if pathlst[-1].strip() in Path.COMMANDS: last_command = command command = pathlst.pop().strip() absolute = (command == command.upper()) @@ -528,7 +561,7 @@ def __str__(self): def __repr__(self): return '' - def segments(self, precision=0): + def segments(self, precision=0) -> list: '''Return a list of segments, each segment is ended by a MoveTo. A segment is a list of Points''' ret = [] @@ -544,7 +577,7 @@ def segments(self, precision=0): return ret - def simplify(self, precision): + def simplify(self, precision:float) -> list: '''Simplify segment with precision: Remove any point which are ~aligned''' ret = [] @@ -554,13 +587,22 @@ def simplify(self, precision): return ret class Ellipse(Transformable): - '''SVG ''' + '''SVG tag handler + An ellipse is created by the center point (center) + the x radius (rx) and the y radius (ry). + Setting these values will change the ellipse + regardless if it was created by an xml element. + + If provided xml has a 'd' attribute or path + then this will also parse that. + (This is for support of inkscape arc objects) + ''' # class Ellipse handles the tag tag = 'ellipse' - arc = False def __init__(self, elt=None): Transformable.__init__(self, elt) + arc = False if elt is not None: self.center = Point(self.xlength(elt.get('cx')), self.ylength(elt.get('cy'))) @@ -571,11 +613,15 @@ def __init__(self, elt=None): self.arc = True self.path = Path(elt) self.path_str = elt.get('d') + else: + self.center = Point(0,0) + self.rx = 0 + self.ry = 0 def __repr__(self): return '' - def bbox(self): + def bbox(self) -> (Point, Point): '''Bounding box''' #TODO change bounding box dependent on rotation pmin = self.center - Point(self.rx, self.ry) @@ -583,6 +629,11 @@ def bbox(self): return (pmin, pmax) def transform(self, matrix=None): + '''Apply the provided matrix. Default (None) + If no matrix is supplied then recursively apply + it's already existing matrix to all items. + Also apply to center, rx, and ry + ''' if matrix is None: matrix = self.matrix else: @@ -591,14 +642,15 @@ def transform(self, matrix=None): self.rx = matrix.xlength(self.rx) self.ry = matrix.ylength(self.ry) - def P(self, t): - '''Return a Point on the Ellipse for t in [0..1]''' + def P(self, t) -> Point: + '''Return a Point on the Ellipse for t in [0..1] or % from angle 0 to the full circle''' #TODO change point cords if rotation is set x = self.center.x + self.rx * math.cos(2 * math.pi * t) y = self.center.y + self.ry * math.sin(2 * math.pi * t) return Point(x,y) - def segments(self, precision=0): + def segments(self, precision=0) -> list: + '''Flatten all curves to segments with target length of precision''' if self.arc: segs = self.path.segments(precision) return segs @@ -619,13 +671,15 @@ def segments(self, precision=0): return [ret] def simplify(self, precision): + '''Return self because a 3 point representation is already simple''' return self # An arc is an ellipse with a beginning and an end point instead of an entire circumference class Arc(Ellipse): - '''SVG ''' - # class Ellipse handles the tag - tag = 'ellipse' + '''This inherits from Ellipse but does not have a svg tag + Because there are no arc tags this class converts the + path data for an arc into an object that can be flattened. + ''' def __init__(self, start_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt): Ellipse.__init__(self, None) @@ -646,6 +700,15 @@ def __repr__(self): return '' def calcuate_center(self): + '''Calculate the center point of the arc from the + non-intuitively provided data in an svg path. + + This is done by creating rotated ellipses around + the start and end point. Then choosing the correct + intersection point based on the two arc choosing flags. + If there is no intersection then the center is the midpoint + between the beginning and end points. + ''' angle = Angle(math.radians(self.rotation)) # set some variables that are used often to decrease size of final equations @@ -663,12 +726,18 @@ def calcuate_center(self): if y != 0: # Finish calculating the line m = ( -2*pts[0].x*rc + 2*pts[1].x*rc - pts[0].y*cs2 + pts[1].y*cs2 ) / -y - b = ( math.pow(pts[0].x,2)*rc - math.pow(pts[1].x,2)*rc + pts[0].x*pts[0].y*cs2 - pts[1].x*pts[1].y*cs2 + math.pow(pts[0].y,2)*(rs) - math.pow(pts[1].y,2)*rs ) / -y + b = ( + math.pow(pts[0].x,2)*rc - math.pow(pts[1].x,2)*rc + pts[0].x*pts[0].y*cs2 - + pts[1].x*pts[1].y*cs2 + math.pow(pts[0].y,2)*(rs) - math.pow(pts[1].y,2)*rs + ) / -y # Now that we have a line we can setup a quadratic equation to solve for all intersection points qa = rc + m*cs2 + math.pow(m,2)*rs qb = -2*pts[0].x*rc + b*cs2 - pts[0].y*cs2 - m*pts[0].x*cs2 + 2*m*b*rs - 2*pts[0].y*m*rs - qc = math.pow(pts[0].x,2)*rc - b*pts[0].x*cs2 + pts[0].x*pts[0].y*cs2 + math.pow(b,2)*rs - 2*b*pts[0].y*rs + math.pow(pts[0].y,2)*rs - math.pow(self.rx*self.ry, 2) + qc = ( + math.pow(pts[0].x,2)*rc - b*pts[0].x*cs2 + pts[0].x*pts[0].y*cs2 + math.pow(b,2)*rs - + 2*b*pts[0].y*rs + math.pow(pts[0].y,2)*rs - math.pow(self.rx*self.ry, 2) + ) else: # When the slope is vertical we need to calculate with x instead of y @@ -679,7 +748,10 @@ def calcuate_center(self): # The quadratic formula but solving for y instead of x and only when the slope is vertical qa = rs qb = x*cs2 - pts[0].x*cs2 - 2*pts[0].y*rs - qc = math.pow(x,2)*rc - 2*x*pts[0].x*rc + math.pow(pts[0].x,2)*rc - x*pts[0].y*cs2 + pts[0].x*pts[0].y*cs2 + math.pow(pts[0].y,2)*rs - math.pow(self.rx*self.ry, 2) + qc = ( + math.pow(x,2)*rc - 2*x*pts[0].x*rc + math.pow(pts[0].x,2)*rc - x*pts[0].y*cs2 + + pts[0].x*pts[0].y*cs2 + math.pow(pts[0].y,2)*rs - math.pow(self.rx*self.ry, 2) + ) # This is the value to see how many real solutions the quadratic equation has. # if root is negative then there are only imaginary solutions or no real solutions @@ -696,8 +768,8 @@ def calcuate_center(self): # Adjust the angle to compensate for ellipse irregularity ptAng = math.atan((self.rx/self.ry) * math.tan(ptAng)) # Calculate scaling factor between provided ellipse and actual end points - radius = math.sqrt(math.pow(self.rx * math.cos(ptAng),2) + math.pow(self.ry * math.sin(ptAng),2)) - dist = math.sqrt( math.pow(self.end_pts[0].x-point.x, 2) + math.pow(self.end_pts[0].y-point.y, 2)) + radius = math.sqrt(math.pow(self.rx*math.cos(ptAng),2) + math.pow(self.ry*math.sin(ptAng),2)) + dist = math.sqrt( math.pow(self.end_pts[0].x-point.x, 2)+math.pow(self.end_pts[0].y-point.y, 2)) factor = dist/radius self.rx *= factor self.ry *= factor @@ -758,14 +830,20 @@ def calcuate_center(self): self.angles[1] += 2*math.pi - def segments(self, precision=0): + def segments(self, precision=0) -> list: + '''This returns segments as expected by the + Path object. (A list of points. Not a list of lists of points) + ''' if max(self.rx, self.ry) < precision: return self.end_pts return Ellipse.segments(self, precision)[0] - def P(self, t): - '''Return a Point on the Arc for t in [0..1]''' + def P(self, t) -> Point: + '''Return a Point on the Arc for t in [0..1] where t is the % from + the start angle to the end angle + ''' #TODO change point cords if rotation is set + # the angles are set in the calculate_center function x = self.center.x + self.rx * math.cos(((self.angles[1] - self.angles[0]) * t) + self.angles[0]) y = self.center.y + self.ry * math.sin(((self.angles[1] - self.angles[0]) * t) + self.angles[0]) return Point(x,y) @@ -774,7 +852,9 @@ def P(self, t): # A circle is a special type of ellipse where rx = ry = radius class Circle(Ellipse): - '''SVG ''' + '''SVG tag handler + This is an ellipse by rx and ry are equal. + ''' # class Circle handles the tag tag = 'circle' @@ -788,7 +868,14 @@ def __repr__(self): return '' class Rect(Transformable): - '''SVG ''' + '''SVG tag handler + This decompiles a rectangle svg xml element into + essentially a path with 4 segments. + + P1 and P2 are the opposing corner points. + + As of now corner radii are not supported. + ''' # class Rect handles the tag tag = 'rect' @@ -807,7 +894,7 @@ def __init__(self, elt=None): def __repr__(self): return '' - def bbox(self): + def bbox(self) -> (Point, Point): '''Bounding box''' xmin = min([p.x for p in (self.P1, self.P2)]) xmax = max([p.x for p in (self.P1, self.P2)]) @@ -817,6 +904,10 @@ def bbox(self): return (Point(xmin,ymin), Point(xmax,ymax)) def transform(self, matrix=None): + '''Apply the provided matrix. Default (None) + If no matrix is supplied then recursively apply + it's already existing matrix to all items. + ''' if matrix is None: matrix = self.matrix else: @@ -824,14 +915,15 @@ def transform(self, matrix=None): self.P1 = matrix * self.P1 self.P2 = matrix * self.P2 - def segments(self, precision=0): - # A rectangle is built with a segment going thru 4 points + def segments(self, precision=0) -> list: + '''A rectangle is built with a segment going thru 4 points''' ret = [] Pa, Pb = Point(0,0),Point(0,0) if self.rotation % 90 == 0: Pa = Point(self.P1.x, self.P2.y) Pb = Point(self.P2.x, self.P1.y) else: + # TODO use builtin rotation function sa = math.sin(math.radians(self.rotation)) / math.cos(math.radians(self.rotation)) sb = -1 / sa ba = -sa * self.P1.x + self.P1.y @@ -846,11 +938,12 @@ def segments(self, precision=0): ret.append([self.P1, Pa, self.P2, Pb, self.P1]) return ret - def simplify(self, precision): - return self.segments(precision) class Line(Transformable): - '''SVG ''' + '''SVG tag handler + + This is essentially a wrapper around the Segment class + ''' # class Line handles the tag tag = 'line' @@ -866,7 +959,7 @@ def __init__(self, elt=None): def __repr__(self): return '' - def bbox(self): + def bbox(self) -> (Point, Point): '''Bounding box''' xmin = min([p.x for p in (self.P1, self.P2)]) xmax = max([p.x for p in (self.P1, self.P2)]) @@ -876,6 +969,10 @@ def bbox(self): return (Point(xmin,ymin), Point(xmax,ymax)) def transform(self, matrix): + '''Apply the provided matrix. Default (None) + If no matrix is supplied then recursively apply + it's already existing matrix to all items. + ''' if matrix is None: matrix = self.matrix else: @@ -884,14 +981,35 @@ def transform(self, matrix): self.P2 = matrix * self.P2 self.segment = Segment(self.P1, self.P2) - def segments(self, precision=0): + def segments(self, precision=0) -> list: + '''Return the segment of the line''' return [self.segment.segments()] - def simplify(self, precision): - return self.segments(precision) class Text(Transformable): - '''SVG ''' + '''SVG tag handler + Take provided xml text element and convert using ttf and otf fonts + into path element that can be used. + + setting Text.default_font is important. If the listed font + cannot be found this is the fallback value. + + A list of fonts installed on the system can be found by calling + Text.load_system_fonts(...) + this keeps all found font in memory after first time call to + improve performance. + + All distinct text element, those that have different start locations + or fonts, are stored in text in a list. + + Adding new strings can be done by calling add_text(...) + and removing strings is done by removing the item from the text list + + Once all strings are properly configured in the text list running + convert_to_path will append a list of path elements to the paths variable + + The bounding box will not report a valid size until convert_to_path has been ran. + ''' # class Text handles the tag tag = 'text' @@ -907,6 +1025,7 @@ def __init__(self, elt=None, parent=None): Transformable.__init__(self, elt) self.bbox_points = [Point(0,0), Point(0,0)] + self.paths = [] if elt is not None: self.style = elt.get('style') @@ -924,6 +1043,12 @@ def __init__(self, elt=None, parent=None): self.text = [] def set_font(self, font=None, bold=None, italic=None, size=None): + '''Set the font of the current text element. + font is expected to be a string of the font family name. + bold is expected Boolean + italic is expected Boolean + size is expected int, but can work with string ending in px + ''' font = font if font else self.font_family bold = bold if bold else (self.bold.lower() != "normal") italic = italic if italic else (self.italic.lower() != "normal") @@ -938,8 +1063,13 @@ def set_font(self, font=None, bold=None, italic=None, size=None): self.font_file = self.find_font_file() - def add_text(self, text, origin=Point(0,0)): - if origin == self.origin: + def add_text(self, text, origin=Point(0,0), inherit=True): + '''Add text the list of text objects + if the origin is not different then the parents origin or + inherit is set to False then a new text element will + be created an added to the strings tuple in the text list. + ''' + if origin == self.origin and inherit: self.text.append((text, self)) else: new_line = Text() @@ -955,6 +1085,13 @@ def add_text(self, text, origin=Point(0,0)): def parse(self, elt, parent): + '''Read the useful data from the xml element. + Since text tags can have nested text tags + parse can be called multiple times for one text tag. + However all nested tags should have parent set so + they can inherit and append the proper values + from their immediate parent + ''' x = elt.get('x') y = elt.get('y') @@ -1006,6 +1143,17 @@ def parse(self, elt, parent): def find_font_file(self): + '''This will look through the indexed fonts and + attempt to find one with a matching font name and text style. + + -- Faux font styles are not supported == + + If the styling cannot be found it will fallback to either + italic or bold if both were asked for and there wasn't a style + with both or regular if italic or bold are set but not found. + + If the target font cannot be found then the default is used if set and found. + ''' if self.font_family is None: if Text.default_font is None: logging.error("Unable to find font because no font was specified.") @@ -1066,6 +1214,16 @@ def find_font_file(self): def convert_to_path(self, auto_transform=True): + ''' Read the vector data from the ttf/otf file and + convert it into a path string for each letter and + parse the path string by a Path instance. + + if auto_transform is True then this calls self.transform() + at the end to apply all transformations on the paths. + + This should only be called once so double check transform() + is never called elsewhere. + ''' self.paths = [] prev_origin = self.text[0][1].origin @@ -1117,26 +1275,34 @@ def convert_to_path(self, auto_transform=True): translate = Matrix([1,0,0,1,offset.x,-size+attrib.origin.y]) * Matrix([scale,0,0,scale,0,0]) # This queues the translations until .transform() is called path[-1].matrix = translate * path[-1].matrix - #path[-1].getTransformations({"transform":"translate({},{}) scale({})".format( + #path[-1].get_transformations({"transform":"translate({},{}) scale({})".format( # offset.x, -size+attrib.origin.y, scale)}) offset.x += (scale*glf.width) self.paths.append(path) - if auto_transform: - self.transform() - - def bbox(self): + #if auto_transform: + # self.transform() + + def bbox(self) -> (Point, Point): + '''Find the bounding box of all the paths that make + each letter. + This will only work if there are available paths. + ''' if self.paths is None or len(self.paths) == 0: return [Point(0,0),Point(0,0)] bboxes = [path.bbox() for paths in self.paths for path in paths] - return [ + return ( Point(min(bboxes, key=lambda v: v[0].x)[0].x, min(bboxes, key=lambda v: v[0].y)[0].y), Point(max(bboxes, key=lambda v: v[1].x)[1].x, max(bboxes, key=lambda v: v[1].y)[1].y), - ] + ) def transform(self, matrix=None): + '''Apply the provided matrix. Default (None) + If no matrix is supplied then recursively apply + it's already existing matrix to all items. + ''' if matrix is None: matrix = self.matrix else: @@ -1146,7 +1312,11 @@ def transform(self, matrix=None): for path in paths: path.transform(matrix) - def segments(self, precision=0): + def segments(self, precision=0) -> list: + '''Get a list of all points in all paths + with provide precision. + This will only work if there are available paths. + ''' segs = [] for paths in self.paths: for path in paths: @@ -1154,8 +1324,19 @@ def segments(self, precision=0): return segs @staticmethod - def load_system_fonts(reload=False): - if len(Text._system_fonts.keys()) < 1 or reload: + def load_system_fonts(reload:bool=False) -> list: + '''Find all fonts in common locations on the file system + To properly read all fonts they need to be parsed so this + is inherently slow on systems with many fonts. + To prevent long parsing time all the results are cached + and the cached results are returned next time this function + is called. + If a force reload of all indexed fonts is desirable setting + reload to True will clear the cache and re-index the system. + ''' + if reload: + Text._system_fonts = {} + if len(Text._system_fonts.keys()) < 1: fonts_files = [] logging.info("Loading system fonts.") for path in Text._os_font_paths[platform.system()]: @@ -1179,9 +1360,10 @@ def load_system_fonts(reload=False): return Text._system_fonts -# overwrite JSONEncoder for svg classes which have defined a .json() method class JSONEncoder(json.JSONEncoder): + ''' overwrite JSONEncoder for svg classes which have defined a .json() method ''' def default(self, obj): + ''' overwrite default function to handle svg classes ''' if not isinstance(obj, tuple(svgClass.values() + [Svg])): return json.JSONEncoder.default(self, obj) @@ -1192,6 +1374,9 @@ def default(self, obj): ## Code executed on module load ## +# Make fontTools more quiet +loggingTools.configLogger(level=logging.INFO) + # SVG tag handler classes are initialized here # (classes must be defined before) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 957b172..342e795 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -134,7 +134,7 @@ def main(): #---------------------------------------------------------------------------- -class LineSegment( object ): +class LineSegment: #------------------------------------------------------------------------ @@ -254,7 +254,7 @@ def __eq__(self, other): #---------------------------------------------------------------------------- -class PolygonSegment( object ): +class PolygonSegment: #------------------------------------------------------------------------ @@ -494,12 +494,12 @@ def process( self, transformer, flip, fill ): #------------------------------------------------------------------------ - # Calculate bounding box of self - def bounding_box(self): - self.bbox = [ + def calc_bbox(self) -> (svg.Point, svg.Point): + '''Calculate bounding box of self''' + self.bbox = ( svg.Point(min(self.points, key=lambda v: v.x).x, min(self.points, key=lambda v: v.y).y), svg.Point(max(self.points, key=lambda v: v.x).x, max(self.points, key=lambda v: v.y).y), - ] + ) #------------------------------------------------------------------------ @@ -530,7 +530,7 @@ def are_distinct(self, polygon): #---------------------------------------------------------------------------- -class Svg2ModImport( object ): +class Svg2ModImport: #------------------------------------------------------------------------ @@ -573,7 +573,7 @@ def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", #---------------------------------------------------------------------------- -class Svg2ModExport( object ): +class Svg2ModExport: #------------------------------------------------------------------------ @@ -599,9 +599,9 @@ def _get_fill_stroke( self, item ): if item.style is not None and item.style != "": - for property in filter(None, item.style.split( ";" )): + for prprty in filter(None, item.style.split( ";" )): - nv = property.split( ":" ) + nv = prprty.split( ":" ) name = nv[ 0 ].strip() value = nv[ 1 ].strip() @@ -745,12 +745,7 @@ def _write_items( self, items, layer, flip = False ): self._write_items( item.items, layer, flip ) continue - elif ( - isinstance( item, svg.Path ) or - isinstance( item, svg.Ellipse) or - isinstance( item, svg.Rect ) or - isinstance( item, svg.Text ) - ): + elif isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text)): segments = [ PolygonSegment( segment ) @@ -766,7 +761,8 @@ def _write_items( self, items, layer, flip = False ): segment.process( self, flip, fill ) if len( segments ) > 1: - [poly.bounding_box() for poly in segments] + for poly in segments: + poly.calc_bbox() segments.sort(key=lambda v: svg.Segment(v.bbox[0], v.bbox[1]).length(), reverse=True) while len(segments) > 0: @@ -1198,7 +1194,7 @@ def _parse_output_file( self ): if line[ : 6 ] == "$INDEX": break - m = re.match( "Units[\s]+mm[\s]*", line ) + m = re.match( r"Units[\s]+mm[\s]*", line ) if m is not None: use_mm = True @@ -1238,13 +1234,10 @@ def _parse_output_file( self ): ) #print( "Pre-index:" ) - #pprint( self.pre_index ) #print( "Post-index:" ) - #pprint( self.post_index ) #print( "Loaded modules:" ) - #pprint( self.loaded_modules ) return use_mm From df8ce0a6892170eebdffacd8a17df8523873dfd5 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 26 Mar 2021 00:02:27 -0600 Subject: [PATCH 089/151] Update license to comply with GPL license cjlango's svg parser was licensed under GPL and to comply with the requirements outlined this project has been relicensed to use a GPL license --- .gitignore | 3 +- .linter/required-linting.rc | 2 +- .travis.yml | 4 +- LICENSE | 454 ++++++++++++++++++++++-------- README.md | 34 +-- setup.py | 4 +- svg2mod/coloredlogger.py | 15 + svg2mod/svg/LICENSE | 339 ---------------------- svg2mod/svg/README.md | 20 -- svg2mod/svg/__init__.py | 5 + svg2mod/svg/{svg => }/geometry.py | 0 svg2mod/svg/{svg => }/svg.py | 37 +-- svg2mod/svg/svg/__init__.py | 11 - svg2mod/svg2mod.py | 377 +++++++++++++++++-------- 14 files changed, 664 insertions(+), 641 deletions(-) delete mode 100644 svg2mod/svg/LICENSE delete mode 100644 svg2mod/svg/README.md rename svg2mod/svg/{svg => }/geometry.py (100%) rename svg2mod/svg/{svg => }/svg.py (98%) delete mode 100644 svg2mod/svg/svg/__init__.py diff --git a/.gitignore b/.gitignore index 030b949..ed50679 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ svg2mod.egg-info *.kicad_mod *.mod __pycache__ -.pytest-cache +.pytest_cache Pipfile Pipfile.lock +.vscode \ No newline at end of file diff --git a/.linter/required-linting.rc b/.linter/required-linting.rc index a1bc3eb..4ae5779 100644 --- a/.linter/required-linting.rc +++ b/.linter/required-linting.rc @@ -75,7 +75,7 @@ enable=c-extension-no-member, bad-indentation, unidiomatic-typecheck, undefined-variable, - #no-member, + no-member, dangerous-default-value, expression-not-assigned, invalid-length-returned, diff --git a/.travis.yml b/.travis.yml index e2351b7..1ea7bcf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,8 @@ jobs: language: python script: - pip install ./ pylint pyenchant - - pylint --rcfile .linter/cleanup.rc svg2mod setup.py - - python setup.py test + - pylint --rcfile .linter/cleanup.rc svg2mod setup.py + - python setup.py test - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug - svg2mod -i examples/svg2mod.svg --debug - svg2mod -i examples/svg2mod.svg --format legacy diff --git a/LICENSE b/LICENSE index 2c4afab..ecbc059 100644 --- a/LICENSE +++ b/LICENSE @@ -1,117 +1,339 @@ -CC0 1.0 Universal - -Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator and -subsequent owner(s) (each and all, an "owner") of an original work of -authorship and/or a database (each, a "Work"). - -Certain owners wish to permanently relinquish those rights to a Work for the -purpose of contributing to a commons of creative, cultural and scientific -works ("Commons") that the public can reliably and without fear of later -claims of infringement build upon, modify, incorporate in other works, reuse -and redistribute as freely as possible in any form whatsoever and for any -purposes, including without limitation commercial purposes. These owners may -contribute to the Commons to promote the ideal of a free culture and the -further production of creative, cultural and scientific works, or to gain -reputation or greater distribution for their Work in part through the use and -efforts of others. - -For these and/or other purposes and motivations, and without any expectation -of additional consideration or compensation, the person associating CC0 with a -Work (the "Affirmer"), to the extent that he or she is an owner of Copyright -and Related Rights in the Work, voluntarily elects to apply CC0 to the Work -and publicly distribute the Work under its terms, with knowledge of his or her -Copyright and Related Rights in the Work and the meaning and intended legal -effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be -protected by copyright and related or neighboring rights ("Copyright and -Related Rights"). Copyright and Related Rights include, but are not limited -to, the following: - - i. the right to reproduce, adapt, distribute, perform, display, communicate, - and translate a Work; - - ii. moral rights retained by the original author(s) and/or performer(s); - - iii. publicity and privacy rights pertaining to a person's image or likeness - depicted in a Work; - - iv. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(a), below; - - v. rights protecting the extraction, dissemination, use and reuse of data in - a Work; - - vi. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation thereof, - including any amended or successor version of such directive); and - - vii. other similar, equivalent or corresponding rights throughout the world - based on applicable law or treaty, and any national implementations thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention of, -applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and -unconditionally waives, abandons, and surrenders all of Affirmer's Copyright -and Related Rights and associated claims and causes of action, whether now -known or unknown (including existing as well as future claims and causes of -action), in the Work (i) in all territories worldwide, (ii) for the maximum -duration provided by applicable law or treaty (including future time -extensions), (iii) in any current or future medium and for any number of -copies, and (iv) for any purpose whatsoever, including without limitation -commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes -the Waiver for the benefit of each member of the public at large and to the -detriment of Affirmer's heirs and successors, fully intending that such Waiver -shall not be subject to revocation, rescission, cancellation, termination, or -any other legal or equitable action to disrupt the quiet enjoyment of the Work -by the public as contemplated by Affirmer's express Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason be -judged legally invalid or ineffective under applicable law, then the Waiver -shall be preserved to the maximum extent permitted taking into account -Affirmer's express Statement of Purpose. In addition, to the extent the Waiver -is so judged Affirmer hereby grants to each affected person a royalty-free, -non transferable, non sublicensable, non exclusive, irrevocable and -unconditional license to exercise Affirmer's Copyright and Related Rights in -the Work (i) in all territories worldwide, (ii) for the maximum duration -provided by applicable law or treaty (including future time extensions), (iii) -in any current or future medium and for any number of copies, and (iv) for any -purpose whatsoever, including without limitation commercial, advertising or -promotional purposes (the "License"). The License shall be deemed effective as -of the date CC0 was applied by Affirmer to the Work. Should any part of the -License for any reason be judged legally invalid or ineffective under -applicable law, such partial invalidity or ineffectiveness shall not -invalidate the remainder of the License, and in such case Affirmer hereby -affirms that he or she will not (i) exercise any of his or her remaining -Copyright and Related Rights in the Work or (ii) assert any associated claims -and causes of action with respect to the Work, in either case contrary to -Affirmer's express Statement of Purpose. - -4. Limitations and Disclaimers. - - a. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - - b. Affirmer offers the Work as-is and makes no representations or warranties - of any kind concerning the Work, express, implied, statutory or otherwise, - including without limitation warranties of title, merchantability, fitness - for a particular purpose, non infringement, or the absence of latent or - other defects, accuracy, or the present or absence of errors, whether or not - discoverable, all to the greatest extent permissible under applicable law. - - c. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without limitation - any person's Copyright and Related Rights in the Work. Further, Affirmer - disclaims responsibility for obtaining any necessary consents, permissions - or other rights required for any use of the Work. - - d. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to this - CC0 or use of the Work. - -For more information, please see - + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/README.md b/README.md index 744b660..86a82d8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # svg2mod + [![Build Status](https://travis-ci.com/svg2mod/svg2mod.svg?branch=main)](https://travis-ci.com/svg2mod/svg2mod) [![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod)](https://github.com/svg2mod/svg2mod/commits/main) @@ -6,8 +7,9 @@ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod)](https://pypi.org/project/svg2mod/) [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version)](https://pypi.org/project/svg2mod/) +This is a program / library to convert SVG drawings to KiCad footprint module files. -This is a small program to convert Inkscape SVG drawings to KiCad footprint module files. It uses [a fork of cjlano's python SVG parser and drawing module](https://github.com/svg2mod/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. +It includes a modified version of [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 @@ -17,13 +19,13 @@ Python 3 ```pip install svg2mod``` - ## Example ```svg2mod -i input.svg``` ## Usage -``` + +```text usage: svg2mod [-h] -i FILENAME [-o FILENAME] [-c] [-pads] [-v] [--debug] [-x] [-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] [--name NAME] [--units UNITS] [--value VALUE] @@ -59,20 +61,21 @@ optional arguments: ## SVG Files svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. This is so it can associate inkscape layers with kicad layers - * 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. - * Paths are fully supported Rect are partially supported. - * A path may have an outline and a fill. (Colors will be ignored.) - * A path may have holes, defined by interior segments within the path (see included examples). - * A path with a filled area inside a hole will not work properly. You must split these apart into two seperate paths. - * 100% Transparent fills and strokes with be ignored. - * Rect supports rotations, but not corner radii. - * Text Elements are not supported - * 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 named to match the target in kicad. The supported layers are listed below. They will be ignored otherwise. - * If there is an issue parsing an inkscape object or stroke convert it to a path. - * Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these elements into paths that will work. + +* 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. +* Paths are fully supported Rect are partially supported. + * A path may have an outline and a fill. (Colors will be ignored.) + * A path may have holes, defined by interior segments within the path (see included examples). + * 100% Transparent fills and strokes with be ignored. + * Rect supports rotations, but not corner radii. + * Text Elements are partially supported +* 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 named to match the target in kicad. The supported layers are listed below. They will be ignored otherwise. +* __If there is an issue parsing an inkscape object or stroke convert it to a path.__ + * __Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these elements into paths that will work.__ ### Layers + This supports the layers listed below. They are the same in inkscape and kicad: | KiCad layer(s) | KiCad legacy | KiCad pretty | @@ -98,4 +101,3 @@ This supports the layers listed below. They are the same in inkscape and kicad: | B.CrtYd | -- | Yes | 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 353bf3e..f654b89 100644 --- a/setup.py +++ b/setup.py @@ -21,8 +21,8 @@ tag = "" try: - tag = subprocess.check_output(["git","describe","--tag"], stderr=subprocess.STDOUT) - tag = tag.stdout.decode('utf-8') + ps = subprocess.check_output(["git","describe","--tag"], stderr=subprocess.STDOUT) + tag = ps.decode('utf-8') tag = tag.replace("-", ".dev", 1).replace("-", "+") except: tag = "0.dev0" diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index ca7d4bd..e8d6026 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -1,3 +1,18 @@ +# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ''' A simple modification to the formatter class in the python logger to allow ANSI color codes based on the logged message's level diff --git a/svg2mod/svg/LICENSE b/svg2mod/svg/LICENSE deleted file mode 100644 index d159169..0000000 --- a/svg2mod/svg/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/svg2mod/svg/README.md b/svg2mod/svg/README.md deleted file mode 100644 index 1e16510..0000000 --- a/svg2mod/svg/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# SVG parser library - ------------------------ - -This is a SVG parser library written in Python and is currently only developed to support -[svg2mod](https://github.com/svg2mod/svg2mod). - - -Capabilities: - - Parse SVG XML - - Apply any transformation (svg transform) - - Explode SVG Path into basic elements (Line, Bezier, ...) - - Interpolate SVG Path as a series of segments - - Able to simplify segments given a precision using Ramer-Douglas-Peucker algorithm - -Not (yet) supported: - - Non-linear transformation drawing (SkewX, ...) - - Text elements (\) - -License: GPLv2+ diff --git a/svg2mod/svg/__init__.py b/svg2mod/svg/__init__.py index 53afb49..6d6bf0c 100644 --- a/svg2mod/svg/__init__.py +++ b/svg2mod/svg/__init__.py @@ -2,5 +2,10 @@ A SVG parser with tools to convert an XML svg file to objects that can be simplified into points. ''' +#__all__ = ['geometry', 'svg'] from .svg import * + +def parse(filename): + '''Take in a filename and return a SVG object of parsed file''' + return Svg(filename) diff --git a/svg2mod/svg/svg/geometry.py b/svg2mod/svg/geometry.py similarity index 100% rename from svg2mod/svg/svg/geometry.py rename to svg2mod/svg/geometry.py diff --git a/svg2mod/svg/svg/svg.py b/svg2mod/svg/svg.py similarity index 98% rename from svg2mod/svg/svg/svg.py rename to svg2mod/svg/svg.py index 77741ea..2d2e08f 100644 --- a/svg2mod/svg/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -22,6 +22,7 @@ ''' import xml.etree.ElementTree as etree +from typing import List, Tuple import math import sys import os @@ -484,7 +485,7 @@ def parse(self, pathstr:str): dimension = {'Q':3, 'C':4} bezier_pts = [] bezier_pts.append(current_pt) - for i in range(1,dimension[command]): + for _ in range(1,dimension[command]): x = pathlst.pop() y = pathlst.pop() pt = Point(x, y) @@ -514,7 +515,7 @@ def parse(self, pathstr:str): # Symmetrical of pt1 against pt0 bezier_pts.append(pt1 + pt1 - pt0) - for i in range(0,nbpts[command]): + for _ in range(0,nbpts[command]): x = pathlst.pop() y = pathlst.pop() pt = Point(x, y) @@ -561,7 +562,7 @@ def __str__(self): def __repr__(self): return '' - def segments(self, precision=0) -> list: + def segments(self, precision=0) -> List[Segment]: '''Return a list of segments, each segment is ended by a MoveTo. A segment is a list of Points''' ret = [] @@ -577,7 +578,7 @@ def segments(self, precision=0) -> list: return ret - def simplify(self, precision:float) -> list: + def simplify(self, precision:float) -> List[Segment]: '''Simplify segment with precision: Remove any point which are ~aligned''' ret = [] @@ -602,7 +603,7 @@ class Ellipse(Transformable): def __init__(self, elt=None): Transformable.__init__(self, elt) - arc = False + self.arc = False if elt is not None: self.center = Point(self.xlength(elt.get('cx')), self.ylength(elt.get('cy'))) @@ -621,7 +622,7 @@ def __init__(self, elt=None): def __repr__(self): return '' - def bbox(self) -> (Point, Point): + def bbox(self) -> Tuple[Point, Point]: '''Bounding box''' #TODO change bounding box dependent on rotation pmin = self.center - Point(self.rx, self.ry) @@ -649,7 +650,7 @@ def P(self, t) -> Point: y = self.center.y + self.ry * math.sin(2 * math.pi * t) return Point(x,y) - def segments(self, precision=0) -> list: + def segments(self, precision=0) -> List[Segment]: '''Flatten all curves to segments with target length of precision''' if self.arc: segs = self.path.segments(precision) @@ -661,7 +662,7 @@ def segments(self, precision=0) -> list: d = 2 * max(self.rx, self.ry) while d > precision: - for (t1,p1),(t2,p2) in zip(p[:-1],p[1:]): + for (t1,_),(t2,_) in zip(p[:-1],p[1:]): t = t1 + (t2 - t1)/2. p.append((t, self.P(t))) p.sort(key=operator.itemgetter(0)) @@ -830,7 +831,7 @@ def calcuate_center(self): self.angles[1] += 2*math.pi - def segments(self, precision=0) -> list: + def segments(self, precision=0) -> List[Segment]: '''This returns segments as expected by the Path object. (A list of points. Not a list of lists of points) ''' @@ -894,7 +895,7 @@ def __init__(self, elt=None): def __repr__(self): return '' - def bbox(self) -> (Point, Point): + def bbox(self) -> Tuple[Point, Point]: '''Bounding box''' xmin = min([p.x for p in (self.P1, self.P2)]) xmax = max([p.x for p in (self.P1, self.P2)]) @@ -915,7 +916,7 @@ def transform(self, matrix=None): self.P1 = matrix * self.P1 self.P2 = matrix * self.P2 - def segments(self, precision=0) -> list: + def segments(self, precision=0) -> List[Segment]: '''A rectangle is built with a segment going thru 4 points''' ret = [] Pa, Pb = Point(0,0),Point(0,0) @@ -959,7 +960,7 @@ def __init__(self, elt=None): def __repr__(self): return '' - def bbox(self) -> (Point, Point): + def bbox(self) -> Tuple[Point, Point]: '''Bounding box''' xmin = min([p.x for p in (self.P1, self.P2)]) xmax = max([p.x for p in (self.P1, self.P2)]) @@ -981,7 +982,7 @@ def transform(self, matrix): self.P2 = matrix * self.P2 self.segment = Segment(self.P1, self.P2) - def segments(self, precision=0) -> list: + def segments(self, precision=0) -> List[Segment]: '''Return the segment of the line''' return [self.segment.segments()] @@ -1111,7 +1112,7 @@ def parse(self, elt, parent): self.font_configs[name] = value if isinstance(self.font_configs["font-size"], str): - float(self.font_configs["font-size"].strip("px")) + self.font_configs["font-size"] = float(self.font_configs["font-size"].strip("px")) for config in self.font_configs: if self.font_configs[config] is None and parent is not None: @@ -1228,7 +1229,7 @@ def convert_to_path(self, auto_transform=True): prev_origin = self.text[0][1].origin offset = Point(prev_origin.x, prev_origin.y) - for index, (text, attrib) in enumerate(self.text): + for text, attrib in self.text: if attrib.font_file is None or attrib.font_family is None: continue @@ -1283,7 +1284,7 @@ def convert_to_path(self, auto_transform=True): #if auto_transform: # self.transform() - def bbox(self) -> (Point, Point): + def bbox(self) -> Tuple[Point, Point]: '''Find the bounding box of all the paths that make each letter. This will only work if there are available paths. @@ -1312,7 +1313,7 @@ def transform(self, matrix=None): for path in paths: path.transform(matrix) - def segments(self, precision=0) -> list: + def segments(self, precision=0) -> List[Segment]: '''Get a list of all points in all paths with provide precision. This will only work if there are available paths. @@ -1324,7 +1325,7 @@ def segments(self, precision=0) -> list: return segs @staticmethod - def load_system_fonts(reload:bool=False) -> list: + def load_system_fonts(reload:bool=False) -> List[dict]: '''Find all fonts in common locations on the file system To properly read all fonts they need to be parsed so this is inherently slow on systems with many fonts. diff --git a/svg2mod/svg/svg/__init__.py b/svg2mod/svg/svg/__init__.py deleted file mode 100644 index 6d6bf0c..0000000 --- a/svg2mod/svg/svg/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -''' -A SVG parser with tools to convert an XML svg file -to objects that can be simplified into points. -''' -#__all__ = ['geometry', 'svg'] - -from .svg import * - -def parse(filename): - '''Take in a filename and return a SVG object of parsed file''' - return Svg(filename) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 342e795..bd482c8 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1,4 +1,29 @@ +# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +''' +This module contains the necessary tools to convert from +the svg objects provided from the svg2mod.svg module to +KiCad file formats. +This currently supports both the pretty format and +the legacy mod format. +''' + +from abc import ABC, abstractmethod +from typing import List, Tuple import argparse import datetime import shlex @@ -17,12 +42,13 @@ DEFAULT_DPI = 96 # 96 as of Inkscape 0.92 def main(): + '''This function handles the scripting package calls. + It is setup to read the arguments from `get_arguments()` + then parse the target svg file and output all converted + objects into a kicad footprint module. + ''' - args, parser = get_arguments() - - pretty = args.format == 'pretty' - use_mm = args.units == 'mm' - + args,_ = get_arguments() # Setup root logger to use terminal colored outputs as well as stdout and stderr @@ -44,11 +70,26 @@ def main(): logging.getLogger("unfiltered").info("Message Here") ''' + if args.list_fonts: + fonts = svg.Text.load_system_fonts() + logging.getLogger("unfiltered").info("Font Name: list of supported styles.") + for font in fonts: + fnt_text = f" {font}:" + for styles in fonts[font]: + fnt_text += f" {styles}," + fnt_text = fnt_text.strip(",") + logging.getLogger("unfiltered").info(fnt_text) + sys.exit(1) + if args.default_font: + svg.Text.default_font = args.default_font + + pretty = args.format == 'pretty' + use_mm = args.units == 'mm' if pretty: if not use_mm: - logging.critical("Error: decimil units only allowed with legacy output type") + logging.critical("Error: decimal units only allowed with legacy output type") sys.exit( -1 ) #if args.include_reverse: @@ -135,6 +176,11 @@ def main(): #---------------------------------------------------------------------------- class LineSegment: + '''Kicad can only draw straight lines. + This class can be type-cast from svg.geometry.Segment + It is designed to have extra functions to help + calculate intersections. + ''' #------------------------------------------------------------------------ @@ -185,7 +231,10 @@ def __init__( self, p = None, q = None ): #------------------------------------------------------------------------ - def connects( self, segment ): + def connects( self, segment: 'LineSegment' ) -> bool: + ''' Return true if provided segment shares + endpoints with the current segment + ''' if self.q.x == segment.p.x and self.q.y == segment.p.y: return True if self.q.x == segment.q.x and self.q.y == segment.q.y: return True @@ -196,7 +245,7 @@ def connects( self, segment ): #------------------------------------------------------------------------ - def intersects( self, segment ): + def intersects( self, segment: 'LineSegment' ) -> bool: """ Return true if line segments 'p1q1' and 'p2q2' intersect. Adapted from: http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/ @@ -237,7 +286,10 @@ def intersects( self, segment ): #------------------------------------------------------------------------ - def q_next( self, q ): + def q_next( self, q:svg.Point ): + '''Shift segment endpoints so self.q is self.p + and q is the new self.q + ''' self.p = self.q self.q = q @@ -255,10 +307,17 @@ def __eq__(self, other): #---------------------------------------------------------------------------- class PolygonSegment: + ''' A polygon should be a collection of segments + creating an enclosed or manifold shape. + This class provides functionality to find overlap + points between a segment and it's self as well as + identify if another polygon rests inside of the + closed area of it's self. + ''' #------------------------------------------------------------------------ - def __init__( self, points): + def __init__( self, points:List): self.points = points @@ -268,12 +327,13 @@ def __init__( self, points): #------------------------------------------------------------------------ - # KiCad will not "pick up the pen" when moving between a polygon outline - # and holes within it, so we search for a pair of points connecting the - # outline (self) or other previously inserted points to the hole such - # that the connecting segment will not cross the visible inner space - # within any hole. - def _find_insertion_point( self, hole, holes, other_insertions ): + def _find_insertion_point( self, hole: 'PolygonSegment', holes: list, other_insertions: list ): + ''' KiCad will not "pick up the pen" when moving between a polygon outline + and holes within it, so we search for a pair of points connecting the + outline (self) or other previously inserted points to the hole such + that the connecting segment will not cross the visible inner space + within any hole. + ''' connected = list( zip(*other_insertions) ) if len(connected) > 0: @@ -288,13 +348,13 @@ def _find_insertion_point( self, hole, holes, other_insertions ): bridge = LineSegment( poly.points[pp], hole_point) trying_new_point = True second_bridge = None - connected_poly = poly + # connected_poly = poly while trying_new_point: trying_new_point = False # Check if bridge passes over other bridges that will be created bad_point = False - for ip, insertion, con_poly in other_insertions: + for ip, insertion,_ in other_insertions: insert = LineSegment( ip, insertion[0]) if bridge.intersects(insert): bad_point = True @@ -320,7 +380,7 @@ def _find_insertion_point( self, hole, holes, other_insertions ): if isinstance(res, bool) and res: break elif isinstance(res, tuple) and len(res) != 0: trying_new_point = True - connected_poly = other_hole + # connected_poly = other_hole if other_hole == hole: hole_point = res[0] bridge = LineSegment( bridge.p, res[0] ) @@ -332,8 +392,6 @@ def _find_insertion_point( self, hole, holes, other_insertions ): else: - logging.info( "[{}, {}]".format( bridge.p, hole_point ) ) - # No other holes intersected, so this insertion point # is acceptable: return ( bridge.p, hole.points_starting_on_index( hole.points.index(hole_point) ), hole ) @@ -351,9 +409,10 @@ def _find_insertion_point( self, hole, holes, other_insertions ): #------------------------------------------------------------------------ - # Return the list of ordered points starting on the given index, ensuring - # that the first and last points are the same. - def points_starting_on_index( self, index ): + def points_starting_on_index( self, index: int ) -> List[svg.Point]: + ''' Return the list of ordered points starting on the given index, ensuring + that the first and last points are the same. + ''' points = self.points @@ -373,8 +432,8 @@ def points_starting_on_index( self, index ): #------------------------------------------------------------------------ - # Return a list of points with the given polygon segments (paths) inlined. - def inline( self, segments ): + def inline( self, segments: List[svg.Point] ) -> List[svg.Point]: + ''' Return a list of points with the given polygon segments (paths) inlined. ''' if len( segments ) < 1: return self.points @@ -412,7 +471,19 @@ def inline( self, segments ): #------------------------------------------------------------------------ - def intersects( self, line_segment, check_connects , count_intersections=False, get_points=False): + def intersects( self, line_segment: LineSegment, check_connects:bool , count_intersections=False, get_points=False): + '''Check to see if line_segment intersects with any + segments of the polygon. Default return True/False + + If check_connects is True then it will skip intersections + that share endpoints with line_segment. + + count_intersections will return the number of intersections + with the polygon. + + get_points returns a tuple of the line that intersects + with line_segment + ''' hole_segment = LineSegment() @@ -425,15 +496,11 @@ def intersects( self, line_segment, check_connects , count_intersections=False, if hole_segment.p is not None: - if ( - check_connects and - line_segment.connects( hole_segment ) - ): continue + if ( check_connects and line_segment.connects( hole_segment )): + continue if line_segment.intersects( hole_segment ): - #print( "Intersection detected." ) - if count_intersections: # If line_segment passes through a point this prevents a second false positive hole_segment.q = None @@ -452,9 +519,10 @@ def intersects( self, line_segment, check_connects , count_intersections=False, #------------------------------------------------------------------------ - # Apply all transformations and rounding, then remove duplicate - # consecutive points along the path. def process( self, transformer, flip, fill ): + ''' Apply all transformations and rounding, then remove duplicate + consecutive points along the path. + ''' points = [] for point in self.points: @@ -494,7 +562,7 @@ def process( self, transformer, flip, fill ): #------------------------------------------------------------------------ - def calc_bbox(self) -> (svg.Point, svg.Point): + def calc_bbox(self) -> Tuple[svg.Point, svg.Point]: '''Calculate bounding box of self''' self.bbox = ( svg.Point(min(self.points, key=lambda v: v.x).x, min(self.points, key=lambda v: v.y).y), @@ -503,8 +571,8 @@ def calc_bbox(self) -> (svg.Point, svg.Point): #------------------------------------------------------------------------ - # Checks if the supplied polygon either contains or insets our bounding box def are_distinct(self, polygon): + ''' Checks if the supplied polygon either contains or insets our bounding box''' distinct = True smaller = min([self, polygon], key=lambda p: svg.Segment(p.bbox[0], p.bbox[1]).length()) @@ -531,8 +599,10 @@ def are_distinct(self, polygon): #---------------------------------------------------------------------------- class Svg2ModImport: + ''' An importer class to read in target svg, + parse it, and keep only layers on interest. + ''' - #------------------------------------------------------------------------ def _prune_hidden( self, items = None ): @@ -573,19 +643,63 @@ def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", #---------------------------------------------------------------------------- -class Svg2ModExport: +class Svg2ModExport(ABC): + ''' An abstract class to provide functionality + to write to kicad module file. + The abstract methods are the file type specific + example: pretty, legacy + ''' + + @property + @abstractmethod + def layer_map(self ): + ''' This should be overwritten by a dictionary object of layer maps ''' + pass + + @abstractmethod + def _get_layer_name( self, name, front ):pass + + @abstractmethod + def _write_library_intro( self, cmdline ): pass + + @abstractmethod + def _get_module_name( self, front = None ): pass + + @abstractmethod + def _write_module_header( self, label_size, label_pen, reference_y, value_y, front,): pass + + @abstractmethod + def _write_modules( self ): pass + + @abstractmethod + def _write_module_footer( self, front ):pass + + @abstractmethod + def _write_polygon_header( self, points, layer ):pass + + @abstractmethod + def _write_polygon( self, points, layer, fill, stroke, stroke_width ):pass + + @abstractmethod + def _write_polygon_footer( self, layer, stroke_width ):pass + + @abstractmethod + def _write_polygon_point( self, point ):pass + + @abstractmethod + def _write_polygon_segment( self, p, q, layer, stroke_width ):pass #------------------------------------------------------------------------ @staticmethod - def _convert_decimil_to_mm( decimil ): - return float( decimil ) * 0.00254 + def _convert_decimal_to_mm( decimal ): + return float( decimal ) * 0.00254 #------------------------------------------------------------------------ @staticmethod - def _convert_mm_to_decimil( mm ): + def _convert_mm_to_decimal( mm ): return int( round( mm * 393.700787 ) ) @@ -634,7 +748,7 @@ def _get_fill_stroke( self, item ): stroke_width = 0.0 elif stroke_width is None: # Give a default stroke width? - stroke_width = self._convert_decimil_to_mm( 1 ) if self.use_mm else 1 + stroke_width = self._convert_decimal_to_mm( 1 ) if self.use_mm else 1 return fill, stroke, stroke_width @@ -671,7 +785,12 @@ def __init__( #------------------------------------------------------------------------ - def add_svg_element(self, elem, layer="F.SilkS"): + def add_svg_element(self, elem : svg.Transformable, layer="F.SilkS"): + ''' This can be used to add a svg element + to a specific layer. + If the importer doesn't have a svg element + it will also create an empty Svg object. + ''' grp = svg.Group() grp.name = layer grp.items.append(elem) @@ -706,8 +825,8 @@ def _calculate_translation( self ): #------------------------------------------------------------------------ - # Find and keep only the layers of interest. def _prune( self, items = None ): + '''Find and keep only the layers of interest.''' if items is None: @@ -801,7 +920,7 @@ def _write_items( self, items, layer, flip = False ): points, layer, fill, stroke, stroke_width ) else: - logging.info( "Skipping {} with 0 points".format(item.__class__.__name__)) + logging.info( " Skipping {} with 0 points".format(item.__class__.__name__)) else: logging.warning( "Unsupported SVG element: {}".format(item.__class__.__name__)) @@ -822,10 +941,10 @@ def _write_module( self, front ): label_pen = 120 if self.use_mm: - label_size = self._convert_decimil_to_mm( label_size ) - label_pen = self._convert_decimil_to_mm( label_pen ) - reference_y = min_point.y - self._convert_decimil_to_mm( label_offset ) - value_y = max_point.y + self._convert_decimil_to_mm( label_offset ) + label_size = self._convert_decimal_to_mm( label_size ) + label_pen = self._convert_decimal_to_mm( label_pen ) + reference_y = min_point.y - self._convert_decimal_to_mm( label_offset ) + value_y = max_point.y + self._convert_decimal_to_mm( label_offset ) else: reference_y = min_point.y - label_offset value_y = max_point.y + label_offset @@ -879,6 +998,9 @@ def _write_polygon_outline( self, points, layer, stroke_width ): #------------------------------------------------------------------------ def transform_point( self, point, flip = False ): + ''' Transform provided point by this + classes scale factor. + ''' transformed_point = svg.Point( ( point.x + self.translation.x ) * self.scale_factor, @@ -901,6 +1023,17 @@ def transform_point( self, point, flip = False ): #------------------------------------------------------------------------ def write( self, cmdline="" ): + '''Write the kicad footprint file. + The value from the command line argument + is set in a comment in the header of the file. + + If self.file_name is not null then this will + overwrite the target file with the data provided. + However if it is null then all data is written + to the string IO class (for same API as writing) + then dumped into self.raw_file_data before the + writer is closed. + ''' self._prune() @@ -929,6 +1062,9 @@ def write( self, cmdline="" ): #---------------------------------------------------------------------------- class Svg2ModExportLegacy( Svg2ModExport ): + ''' A child of Svg2ModExport that implements + specific functionality for kicad legacy file types + ''' layer_map = { #'inkscape-name' : [ kicad-front, kicad-back ], @@ -1021,23 +1157,19 @@ def _write_library_intro( self, cmdline ): # Converted using: {3} # """.format( - datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ), - units, - modules_list, - cmdline.replace("\\","\\\\") -) + datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ), + units, + modules_list, + cmdline.replace("\\","\\\\") + ) ) #------------------------------------------------------------------------ def _write_module_header( - self, - label_size, - label_pen, - reference_y, - value_y, - front, + self, label_size, label_pen, + reference_y, value_y, front, ): self.output_file.write( """$MODULE {0} @@ -1046,14 +1178,14 @@ def _write_module_header( T0 0 {1} {2} {2} 0 {3} N I 21 "{0}" T1 0 {5} {2} {2} 0 {3} N I 21 "{4}" """.format( - self._get_module_name( front ), - reference_y, - label_size, - label_pen, - self.imported.module_value, - value_y, - 15, # Seems necessary -) + self._get_module_name( front ), + reference_y, + label_size, + label_pen, + self.imported.module_value, + value_y, + 15, # Seems necessary + ) ) @@ -1107,7 +1239,7 @@ def _write_polygon_header( self, points, layer ): pen = 1 if self.use_mm: - pen = self._convert_decimil_to_mm( pen ) + pen = self._convert_decimal_to_mm( pen ) self.output_file.write( "DP 0 0 0 0 {} {} {}\n".format( len( points ), @@ -1142,6 +1274,10 @@ def _write_polygon_segment( self, p, q, layer, stroke_width ): #---------------------------------------------------------------------------- class Svg2ModExportLegacyUpdater( Svg2ModExportLegacy ): + ''' A Svg2Mod exporter class that reads some settings + from an already existing module and will append its + changes to the file. + ''' #------------------------------------------------------------------------ @@ -1233,12 +1369,6 @@ def _parse_output_file( self ): "Expected $EndLIBRARY: [{}]".format( line ) ) - #print( "Pre-index:" ) - - #print( "Post-index:" ) - - #print( "Loaded modules:" ) - return use_mm @@ -1366,6 +1496,10 @@ def _write_module_header( #---------------------------------------------------------------------------- class Svg2ModExportPretty( Svg2ModExport ): + ''' This provides functionality for the + newer kicad "pretty" footprint file formats. + It is a child of Svg2ModExport. + ''' layer_map = { #'inkscape-name' : kicad-name, @@ -1414,13 +1548,13 @@ def _write_library_intro( self, cmdline ): (descr "{2}") (tags {3}) """.format( - self.imported.module_name, #0 - int( round( #1 - os.path.getctime( self.imported.file_name ) if self.imported.file_name else time.time() - ) ), - "Converted using: {}".format( cmdline.replace("\\", "\\\\") ), #2 - "svg2mod", #3 -) + self.imported.module_name, #0 + int( round( #1 + os.path.getctime( self.imported.file_name ) if self.imported.file_name else time.time() + ) ), + "Converted using: {}".format( cmdline.replace("\\", "\\\\") ), #2 + "svg2mod", #3 + ) ) @@ -1434,12 +1568,8 @@ def _write_module_footer( self, front ): #------------------------------------------------------------------------ def _write_module_header( - self, - label_size, - label_pen, - reference_y, - value_y, - front, + self, label_size, label_pen, + reference_y, value_y, front, ): if front: side = "F" @@ -1454,14 +1584,14 @@ def _write_module_header( (effects (font (size {3} {3}) (thickness {4}))) )""".format( - self._get_module_name(), #0 - reference_y, #1 - side, #2 - label_size, #3 - label_pen, #4 - self.imported.module_value, #5 - value_y, #6 -) + self._get_module_name(), #0 + reference_y, #1 + side, #2 + label_size, #3 + label_pen, #4 + self.imported.module_value, #5 + value_y, #6 + ) ) @@ -1569,11 +1699,11 @@ def _write_polygon_segment( self, p, q, layer, stroke_width ): (layer {}) (width {}) )""".format( - p.x, p.y, - q.x, q.y, - layer, - stroke_width, -) + p.x, p.y, + q.x, q.y, + layer, + stroke_width, + ) ) @@ -1582,6 +1712,9 @@ def _write_polygon_segment( self, p, q, layer, stroke_width ): #---------------------------------------------------------------------------- def get_arguments(): + ''' Return an instance of pythons argument parser + with all the command line functionalities arguments + ''' parser = argparse.ArgumentParser( description = ( @@ -1589,15 +1722,14 @@ def get_arguments(): ) ) - #------------------------------------------------------------------------ + mux = parser.add_mutually_exclusive_group(required=True) - parser.add_argument( + mux.add_argument( '-i', '--input-file', type = str, dest = 'input_file_name', metavar = 'FILENAME', - help = "name of the SVG file", - required = True, + help = "Name of the SVG file", ) parser.add_argument( @@ -1605,7 +1737,7 @@ def get_arguments(): type = str, dest = 'output_file_name', metavar = 'FILENAME', - help = "name of the module file", + help = "Name of the module file", ) parser.add_argument( @@ -1667,7 +1799,7 @@ def get_arguments(): type = float, dest = 'scale_factor', metavar = 'FACTOR', - help = "scale paths by this factor", + help = "Scale paths by this factor", default = 1.0, ) @@ -1676,7 +1808,7 @@ def get_arguments(): type = float, dest = 'precision', metavar = 'PRECISION', - help = "smoothness for approximating curves with line segments. Input is the approximate length for each line segment in SVG pixels (float)", + help = "Smoothness for approximating curves with line segments. Input is the approximate length for each line segment in SVG pixels (float)", default = 10.0, ) parser.add_argument( @@ -1685,7 +1817,7 @@ def get_arguments(): dest = 'format', metavar = 'FORMAT', choices = [ 'legacy', 'pretty' ], - help = "output module file format (legacy|pretty)", + help = "Output module file format (legacy|pretty)", default = 'pretty', ) @@ -1694,7 +1826,7 @@ def get_arguments(): type = str, dest = 'module_name', metavar = 'NAME', - help = "base name of the module", + help = "Base name of the module", default = "svg2mod", ) @@ -1703,8 +1835,8 @@ def get_arguments(): type = str, dest = 'units', metavar = 'UNITS', - choices = [ 'decimil', 'mm' ], - help = "output units, if output format is legacy (decimil|mm)", + choices = [ 'decimal', 'mm' ], + help = "Output units, if output format is legacy (decimal|mm)", default = 'mm', ) @@ -1713,12 +1845,27 @@ def get_arguments(): type = str, dest = 'module_value', metavar = 'VALUE', - help = "value of the module", + help = "Value of the module", default = "G***", ) - return parser.parse_args(), parser + parser.add_argument( + '-df', '--default-font', + type = str, + dest = 'default_font', + help = "Default font to use if the target font in a text element cannot be found", + ) + + mux.add_argument( + '-lsf', '--list-fonts', + dest = 'list_fonts', + const = True, + default = False, + action = "store_const", + help = "List all fonts that can be found in common locations", + ) + return parser.parse_args(), parser #------------------------------------------------------------------------ From 75ebcb8dca42678f79a4afcbd9a008b5cbcb53ce Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 26 Mar 2021 00:09:16 -0600 Subject: [PATCH 090/151] Update setup.py to reflect new license --- setup.cfg | 3 +++ setup.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index ad84911..d48d073 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,6 @@ test=pytest [tool:pytest] addopts = --pylint --pylint-rcfile=.linter/required-linting.rc + +[metadata] +license_files = LICENSE \ No newline at end of file diff --git a/setup.py b/setup.py index f654b89..71d1d79 100644 --- a/setup.py +++ b/setup.py @@ -54,13 +54,13 @@ package_data={'kipart': ['*.gif', '*.png']}, scripts=[], install_requires=requirements, - license="CC0-1.0", + license="GPLV2", zip_safe=False, keywords='svg2mod, KiCAD, inkscape', classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', - 'License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication', + 'LICENSE :: OSI APPROVED :: GNU GENERAL PUBLIC LICENSE V2 (GPLV2)', 'Natural Language :: English', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', From 25085c74c2ac14b579294aee7342365d4f0893f7 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 26 Mar 2021 22:07:41 -0600 Subject: [PATCH 091/151] Update .travis.yml --- .travis.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1ea7bcf..d27b5d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,14 @@ jobs: - stage: unit tests os: linux language: python - script: + python: + - "3.7" + - "3.8" + - "3.9" + before_install: + - apt install python-enchant - pip install ./ pylint pyenchant + script: - pylint --rcfile .linter/cleanup.rc svg2mod setup.py - python setup.py test - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug From 795c41abb90c00068cade6d3558279768c9b6c7e Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 26 Mar 2021 22:10:10 -0600 Subject: [PATCH 092/151] Update .travis.yml --- .travis.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index d27b5d9..3f272ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,24 +4,24 @@ jobs: os: linux language: python python: - - "3.7" - - "3.8" - - "3.9" + - "3.7" + - "3.8" + - "3.9" before_install: - - apt install python-enchant - - pip install ./ pylint pyenchant + - sudo apt-get -y install python-enchant + - pip install ./ pylint pyenchant script: - - pylint --rcfile .linter/cleanup.rc svg2mod setup.py - - python setup.py test - - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug - - svg2mod -i examples/svg2mod.svg --debug - - svg2mod -i examples/svg2mod.svg --format legacy - - svg2mod -i examples/svg2mod.svg + - pylint --rcfile .linter/cleanup.rc svg2mod setup.py + - python setup.py test + - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug + - svg2mod -i examples/svg2mod.svg --debug + - svg2mod -i examples/svg2mod.svg --format legacy + - svg2mod -i examples/svg2mod.svg - stage: deploy os: linux language: python script: - - python setup.py bdist_wheel sdist + - python setup.py bdist_wheel sdist deploy: provider: pypi username: "__token__" From 0133458b893a9f3c61a657d2bd887891a65ce6b7 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 27 Mar 2021 09:53:59 -0600 Subject: [PATCH 093/151] Update .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3f272ba..3afdaf9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ jobs: - "3.8" - "3.9" before_install: - - sudo apt-get -y install python-enchant + - sudo apt-get -y install --install-recommends python-enchant - pip install ./ pylint pyenchant script: - pylint --rcfile .linter/cleanup.rc svg2mod setup.py From 26d9c5f228dd9062a80d074db9fc1c02cb5306c4 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 27 Mar 2021 09:33:22 -0600 Subject: [PATCH 094/151] Clean up spelling --- .linter/custom_dict | 7 +++++++ svg2mod/svg/geometry.py | 2 +- svg2mod/svg/svg.py | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.linter/custom_dict b/.linter/custom_dict index 9721dda..289e0ac 100644 --- a/.linter/custom_dict +++ b/.linter/custom_dict @@ -36,3 +36,10 @@ xml iterable decompiles py +API +DPI +Wikipedia +poly +Regex +thru +Faux diff --git a/svg2mod/svg/geometry.py b/svg2mod/svg/geometry.py index 0211c8d..2257619 100644 --- a/svg2mod/svg/geometry.py +++ b/svg2mod/svg/geometry.py @@ -188,7 +188,7 @@ def pdistance(self, p): # Vertical Segment => pdistance is the difference of abscissa return abs(self.start.x - p.x) else: - # That's 2-D perpendicular distance formulae (ref: Wikipedia) + # That's 2-D perpendicular distance formula (ref: Wikipedia) slope = s.y/s.x # intercept: Crossing with ordinate y-axis intercept = self.start.y - (slope * self.start.x) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 2d2e08f..b84380d 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -924,7 +924,7 @@ def segments(self, precision=0) -> List[Segment]: Pa = Point(self.P1.x, self.P2.y) Pb = Point(self.P2.x, self.P1.y) else: - # TODO use builtin rotation function + # TODO use built-in rotation function sa = math.sin(math.radians(self.rotation)) / math.cos(math.radians(self.rotation)) sb = -1 / sa ba = -sa * self.P1.x + self.P1.y @@ -993,7 +993,7 @@ class Text(Transformable): into path element that can be used. setting Text.default_font is important. If the listed font - cannot be found this is the fallback value. + cannot be found this is the fall back value. A list of fonts installed on the system can be found by calling Text.load_system_fonts(...) @@ -1149,7 +1149,7 @@ def find_font_file(self): -- Faux font styles are not supported == - If the styling cannot be found it will fallback to either + If the styling cannot be found it will fall back to either italic or bold if both were asked for and there wasn't a style with both or regular if italic or bold are set but not found. From 9a193bf5eaf3dd4620ccd3d0d6870a69e56b9485 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 27 Mar 2021 09:36:27 -0600 Subject: [PATCH 095/151] Update svg2mod/svg/svg.py --- svg2mod/svg/svg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index b84380d..623ad80 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -1382,7 +1382,7 @@ def default(self, obj): # (classes must be defined before) svgClass = {} -# Register all classes with attribute 'tag' in svgClass dict +# Register all classes with attribute 'tag' in svgClass dictionary for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): tag = getattr(cls, 'tag', None) if tag: From deed8eae164aaacd4215ea02b82a0163cff43851 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 27 Mar 2021 10:41:44 -0600 Subject: [PATCH 096/151] Fix license name --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 71d1d79..039ecf3 100644 --- a/setup.py +++ b/setup.py @@ -54,13 +54,13 @@ package_data={'kipart': ['*.gif', '*.png']}, scripts=[], install_requires=requirements, - license="GPLV2", + license="GPLv2+", zip_safe=False, keywords='svg2mod, KiCAD, inkscape', classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Science/Research', - 'LICENSE :: OSI APPROVED :: GNU GENERAL PUBLIC LICENSE V2 (GPLV2)', + 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', 'Natural Language :: English', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', From 9aff2dd10321190092fb52bba0f1675601e1bc95 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 5 Apr 2021 13:53:45 -0600 Subject: [PATCH 097/151] Fix command line arguments and documentation --- .travis.yml | 2 +- README.md | 32 ++++++++++++++++++-------------- svg2mod/svg2mod.py | 6 +++--- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3afdaf9..eb1c7e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ jobs: script: - pylint --rcfile .linter/cleanup.rc svg2mod setup.py - python setup.py test - - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -pads --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug + - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -P --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug - svg2mod -i examples/svg2mod.svg --debug - svg2mod -i examples/svg2mod.svg --format legacy - svg2mod -i examples/svg2mod.svg diff --git a/README.md b/README.md index 86a82d8..153a04c 100644 --- a/README.md +++ b/README.md @@ -26,37 +26,41 @@ Python 3 ## Usage ```text -usage: svg2mod [-h] -i FILENAME [-o FILENAME] [-c] [-pads] [-v] [--debug] [-x] +usage: svg2mod [-h] [-i FILENAME] [-o FILENAME] [-c] [-P] [-v] [--debug] [-x] [-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] - [--name NAME] [--units UNITS] [--value VALUE] + [--name NAME] [--units UNITS] [--value VALUE] [-F DEFAULT_FONT] + [-l] Convert Inkscape SVG drawings to KiCad footprint modules. optional arguments: -h, --help show this help message and exit -i FILENAME, --input-file FILENAME - name of the SVG file + Name of the SVG file -o FILENAME, --output-file FILENAME - name of the module file + Name of the module file -c, --center Center the module to the center of the bounding box - -pads, --convert-pads - Convert any artwork on Cu layers to pads + -P, --convert-pads Convert any artwork on Cu layers to pads -v, --verbose Print more verbose messages --debug Print debug level messages -x, --exclude-hidden Do not export hidden layers -d DPI, --dpi DPI DPI of the SVG file (int) -f FACTOR, --factor FACTOR - scale paths by this factor + Scale paths by this factor -p PRECISION, --precision PRECISION - smoothness for approximating curves with line segments - (float) - --format FORMAT output module file format (legacy|pretty) + Smoothness for approximating curves with line + segments. Input is the approximate length for each + line segment in SVG pixels (float) + --format FORMAT Output module file format (legacy|pretty) --name NAME, --module-name NAME - base name of the module - --units UNITS output units, if output format is legacy (decimil|mm) + Base name of the module + --units UNITS Output units, if output format is legacy (decimal|mm) --value VALUE, --module-value VALUE - value of the module -``` + Value of the module + -F DEFAULT_FONT, --default-font DEFAULT_FONT + Default font to use if the target font in a text + element cannot be found + -l, --list-fonts List all fonts that can be found in common locations``` ## SVG Files diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index bd482c8..6afb3f9 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1750,7 +1750,7 @@ def get_arguments(): ) parser.add_argument( - '-pads', '--convert-pads', + '-P', '--convert-pads', dest = 'convert_to_pads', action = 'store_const', const = True, @@ -1850,14 +1850,14 @@ def get_arguments(): ) parser.add_argument( - '-df', '--default-font', + '-F', '--default-font', type = str, dest = 'default_font', help = "Default font to use if the target font in a text element cannot be found", ) mux.add_argument( - '-lsf', '--list-fonts', + '-l', '--list-fonts', dest = 'list_fonts', const = True, default = False, From e618143d5b93f4cac5f22a5b157627a7c345da18 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 5 Apr 2021 13:55:22 -0600 Subject: [PATCH 098/151] Fix missing ticks --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 153a04c..2226e82 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ optional arguments: Default font to use if the target font in a text element cannot be found -l, --list-fonts List all fonts that can be found in common locations``` +``` ## SVG Files From 5b105134c195671d77f1f15c3061741ad9add9b9 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 7 Apr 2021 12:55:25 -0600 Subject: [PATCH 099/151] Fix a bug with text parsing and hole inlining Inlining points maintained needless precision leading to execive computation times. Automatic transformations applied to text was not working --- .travis.yml | 1 + svg2mod/svg/geometry.py | 4 ++++ svg2mod/svg/svg.py | 4 ++-- svg2mod/svg2mod.py | 30 ++++++++++++++++++++++-------- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index eb1c7e8..ea506f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ jobs: - svg2mod -i examples/svg2mod.svg --debug - svg2mod -i examples/svg2mod.svg --format legacy - svg2mod -i examples/svg2mod.svg + - svg2mod -l - stage: deploy os: linux language: python diff --git a/svg2mod/svg/geometry.py b/svg2mod/svg/geometry.py index 2257619..3ad6431 100644 --- a/svg2mod/svg/geometry.py +++ b/svg2mod/svg/geometry.py @@ -128,6 +128,10 @@ def rot(self, angle, x=0, y=0): new_y = ((self.x-x) * angle.sin) + ((self.y-y) * angle.cos) + y return Point(new_x,new_y) + def round(self, ndigits=None): + '''Round x and y to number of decimal points''' + return Point( round(self.x, ndigits), round(self.y, ndigits)) + class Angle: '''Define a trigonometric angle [of a vector] ''' diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 623ad80..349ee80 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -1281,8 +1281,8 @@ def convert_to_path(self, auto_transform=True): offset.x += (scale*glf.width) self.paths.append(path) - #if auto_transform: - # self.transform() + if auto_transform: + self.transform() def bbox(self) -> Tuple[Point, Point]: '''Find the bounding box of all the paths that make diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 6afb3f9..4560049 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -236,10 +236,10 @@ def connects( self, segment: 'LineSegment' ) -> bool: endpoints with the current segment ''' - if self.q.x == segment.p.x and self.q.y == segment.p.y: return True - if self.q.x == segment.q.x and self.q.y == segment.q.y: return True - if self.p.x == segment.p.x and self.p.y == segment.p.y: return True - if self.p.x == segment.q.x and self.p.y == segment.q.y: return True + if self.q == segment.p: return True + if self.q == segment.q: return True + if self.p == segment.p: return True + if self.p == segment.q: return True return False @@ -313,13 +313,23 @@ class PolygonSegment: points between a segment and it's self as well as identify if another polygon rests inside of the closed area of it's self. + + When initializing this class it will round all + points to the specified number of decimal points, + default 10, and remove duplicate points in a row. ''' #------------------------------------------------------------------------ - def __init__( self, points:List): + def __init__( self, points:List, ndigits=10): + + self.points = [points.pop(0).round(ndigits)] + + for point in points: + p = point.round(ndigits) + if self.points[-1] != p: + self.points.append(p) - self.points = points if len( points ) < 3: logging.warning("Warning: Path segment has only {} points (not a polygon?)".format(len( points ))) @@ -440,6 +450,10 @@ def inline( self, segments: List[svg.Point] ) -> List[svg.Point]: logging.debug( " Inlining {} segments...".format( len( segments ) ) ) + segments.sort(key=lambda h: + svg.Segment(self.points[0], h.bbox[0]).length() + ) + all_segments = segments[ : ] + [ self ] insertions = [] @@ -512,7 +526,7 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int if count_intersections: return intersections - if get_points: + if get_points and not check_connects: return () return False @@ -1022,7 +1036,7 @@ def transform_point( self, point, flip = False ): #------------------------------------------------------------------------ - def write( self, cmdline="" ): + def write( self, cmdline="scripting" ): '''Write the kicad footprint file. The value from the command line argument is set in a comment in the header of the file. From d4741c7add85160feb821bffc0f3aac51bb53c23 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 7 Apr 2021 13:13:36 -0600 Subject: [PATCH 100/151] Fix exit code when using -l --- svg2mod/svg2mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 4560049..c6fb469 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -79,7 +79,7 @@ def main(): fnt_text += f" {styles}," fnt_text = fnt_text.strip(",") logging.getLogger("unfiltered").info(fnt_text) - sys.exit(1) + sys.exit(0) if args.default_font: svg.Text.default_font = args.default_font From eb67056c6ac058339b56df1d13ab835b6e303e41 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 10 Apr 2021 10:46:58 -0600 Subject: [PATCH 101/151] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2226e82..4c64b5f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ It includes a modified version of [cjlano's python SVG parser and drawing module ## Requirements -Python 3 +* Python 3 +* [fonttools](https://pypi.org/project/fonttools/) ## Installation From dab7905207da77d0a5121ac776c4b868c70d7432 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 17 Dec 2021 17:00:25 -0700 Subject: [PATCH 102/151] Bug fixes and better font parsing Thanks to @gregdavill for writing the improved font converting --- setup.py | 2 +- svg2mod/svg/svg.py | 43 ++++++++++++++++--------------------------- svg2mod/svg2mod.py | 4 ++-- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/setup.py b/setup.py index 039ecf3..36bfe06 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ try: ps = subprocess.check_output(["git","describe","--tag"], stderr=subprocess.STDOUT) - tag = ps.decode('utf-8') + tag = ps.decode('utf-8').strip() tag = tag.replace("-", ".dev", 1).replace("-", "+") except: tag = "0.dev0" diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 349ee80..171a93e 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -36,8 +36,7 @@ import inspect from fontTools.ttLib import ttFont -from fontTools.pens.recordingPen import RecordingPen -from fontTools.pens.basePen import decomposeQuadraticSegment +from fontTools.pens.svgPathPen import SVGPathPen from fontTools.misc import loggingTools from .geometry import Point,Angle,Segment,Bezier,MoveTo,simplify_segment @@ -1105,6 +1104,7 @@ def parse(self, elt, parent): } if self.style is not None: for style in self.style.split(";"): + if style.find(":") == -1: continue nv = style.split(":") name = nv[ 0 ].strip() value = nv[ 1 ].strip() @@ -1226,6 +1226,7 @@ def convert_to_path(self, auto_transform=True): is never called elsewhere. ''' self.paths = [] + if not self.text: return prev_origin = self.text[0][1].origin offset = Point(prev_origin.x, prev_origin.y) @@ -1246,38 +1247,26 @@ def convert_to_path(self, auto_transform=True): for char in text: pathbuf = "" - pen = RecordingPen() try: glf = ttf.getGlyphSet()[ttf.getBestCmap()[ord(char)]] except KeyError: logging.warning(f"Unsuported character in element \"{char}\"") #txt = txt.replace(char, "") continue + pen = SVGPathPen(ttf.getGlyphSet) glf.draw(pen) - for command in pen.value: - pts = list(command[1]) - for ptInd in range(len(pts)): - pts[ptInd] = (pts[ptInd][0], offset.y - pts[ptInd][1]) - if command[0] == "moveTo" or command[0] == "lineTo": - pathbuf += command[0][0].upper() + f" {pts[0][0]},{pts[0][1]} " - elif command[0] == "qCurveTo": - pts = decomposeQuadraticSegment(command[1]) - for pt in pts: - pathbuf += "Q {},{} {},{} ".format( - pt[0][0], offset.y - pt[0][1], - pt[1][0], offset.y - pt[1][1] - ) - elif command[0] == "closePath": - pathbuf += "Z" - - path.append(Path()) - path[-1].parse(pathbuf) - # Apply the scaling then the translation - translate = Matrix([1,0,0,1,offset.x,-size+attrib.origin.y]) * Matrix([scale,0,0,scale,0,0]) - # This queues the translations until .transform() is called - path[-1].matrix = translate * path[-1].matrix - #path[-1].get_transformations({"transform":"translate({},{}) scale({})".format( - # offset.x, -size+attrib.origin.y, scale)}) + + for cmd in pen._commands: + pathbuf += cmd + ' ' + + if len(pathbuf) > 0: + path.append(Path()) + path[-1].parse(pathbuf) + # Apply the scaling then the translation + translate = Matrix([1,0,0,-1,offset.x,size+attrib.origin.y]) * Matrix([scale,0,0,scale,0,0]) + # This queues the translations until .transform() is called + path[-1].matrix = translate * path[-1].matrix + offset.x += (scale*glf.width) self.paths.append(path) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index c6fb469..ee270a3 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -632,7 +632,7 @@ def _prune_hidden( self, items = None ): if item.hidden : logging.warning("Ignoring hidden SVG layer: {}".format( item.name ) ) - elif item.name is not "": + elif item.name != "": self.svg.items.append( item ) if(item.items): @@ -858,7 +858,7 @@ def _prune( self, items = None ): for name in self.layers.keys(): #if re.search( name, item.name, re.I ): - if name == item.name and item.name is not "": + if name == item.name and item.name != "": logging.getLogger("unfiltered").info( "Found SVG layer: {}".format( item.name ) ) self.imported.svg.items.append( item ) From bd6195db308258b831907601f9cd22ac9b28adaf Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 20 Dec 2021 09:34:22 -0700 Subject: [PATCH 103/151] Move from travis-ci to github actions --- .github/workflows/pylint.yml | 23 +++++++++++++++ .github/workflows/python-package.yml | 44 ++++++++++++++++++++++++++++ .github/workflows/python-publish.yml | 38 ++++++++++++++++++++++++ .travis.yml | 34 --------------------- setup.py | 4 ++- 5 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/pylint.yml create mode 100644 .github/workflows/python-package.yml create mode 100644 .github/workflows/python-publish.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 0000000..e973342 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,23 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Analysing the code with pylint + run: | + pylint --rcfile .linger/cleanup.rc svg2mod setup.py diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..5de1a5f --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,44 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pytest pylint pyenchant ./ + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with pytlint + run: | + pylint --rcfile .linter/cleanup.rc svg2mod setup.py + - name: Test with pytest + run: | + python setup.py test + - name: Run svg tests + run: | + svg2mod -i examples/svg2mod.svg -o output.mod -x -c -P --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug + svg2mod -i examples/svg2mod.svg --debug + svg2mod -i examples/svg2mod.svg --format legacy + svg2mod -i examples/svg2mod.svg + svg2mod -l diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..5d7da0a --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,38 @@ +# This workflow will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Upload Python Package + +on: + release: + types: [published] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install wheel setuptools + - name: Build package + run: python setup.py bdist_wheel sdist + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ea506f8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -jobs: - include: - - stage: unit tests - os: linux - language: python - python: - - "3.7" - - "3.8" - - "3.9" - before_install: - - sudo apt-get -y install --install-recommends python-enchant - - pip install ./ pylint pyenchant - script: - - pylint --rcfile .linter/cleanup.rc svg2mod setup.py - - python setup.py test - - svg2mod -i examples/svg2mod.svg -o output.mod -x -c -P --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug - - svg2mod -i examples/svg2mod.svg --debug - - svg2mod -i examples/svg2mod.svg --format legacy - - svg2mod -i examples/svg2mod.svg - - svg2mod -l - - stage: deploy - os: linux - language: python - script: - - python setup.py bdist_wheel sdist - deploy: - provider: pypi - username: "__token__" - password: - secure: "VtbTcUsotXbHcGyVXT0xrErRHt3j104GX0LzuvKEc/uRLGr+Eo3CUHguFSanQ7p2LH2k0DTEmrzMfLVHltXLPmAqvUL3MrM0oUSCf4bkPqcLWiGhHYYtWs4XiYs3aS7esM0SKOyFBHZ1z/Y9JoKIywZgiMD/CI/XqMI5IHfDmy9XAxlqgTN0COp2NWP9SGi2VdJ0sebm7jVGt8kp99yq/2F+VDSADutoVhx68+XMdgd/JMm9Q8ouSra0ni9jpfyN8JJ59ucj8i2LUhz14+zQGcMAYm0QbIhKuibbHsDImLp1vxubAwbvaeA6K5jerZKme8wLAvilLm17bly5RivHER21IW53hccVTRxIzkt3nhJfaU0XBbJ8nxJJJ7F6Rrpzn6Tpcvx4WgODA/0nQ1DRUoGXBiLPO987q7SaYJdP4Ay4dZSTBXt1Rv039IZvTHs0M0MORXB4lSBQ0S0oAJzOA74+jptTW+z+rktXXL8yPBxSIbq7wSPOmrl56gxhojveiXVh6t51+HT16bV4gFRB3SYacDFxSN5ZAGI4ziaBEFYm/PTP5HHciat7qMP7I/8QhV1pkl7IXlENC1I0miYTQN77jzE9Z8RU2rEtYzZBNH5M5agr2AmFrTRu2HD+fINcAyMHvDyrYCnonss7oWjzoJb8PqpkDpSyNsrYGFyi1VU=" - distributions: "sdist bdist_wheel" - on: - branch: main - tags: true diff --git a/setup.py b/setup.py index 36bfe06..1e1f39c 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,9 @@ 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', 'Natural Language :: English', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], setup_requires=setup_requirements, test_suite='test', From b95b54843b982f0a46bc7d4850403f71e62cca37 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 20 Dec 2021 10:51:06 -0700 Subject: [PATCH 104/151] Delete pylint.yml --- .github/workflows/pylint.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/pylint.yml diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml deleted file mode 100644 index e973342..0000000 --- a/.github/workflows/pylint.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Pylint - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.8", "3.9", "3.10"] - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - - name: Analysing the code with pylint - run: | - pylint --rcfile .linger/cleanup.rc svg2mod setup.py From b7529094602a830de69f5eb783670d6df7359c1f Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 20 Dec 2021 10:55:44 -0700 Subject: [PATCH 105/151] Remove pypi testing url --- .github/workflows/python-publish.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 5d7da0a..9cb7e23 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -34,5 +34,4 @@ jobs: with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} - repository_url: https://test.pypi.org/legacy/ From f728eac018fecd2cbd8f3d12869040c06c56eaa4 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 20 Dec 2021 11:25:16 -0700 Subject: [PATCH 106/151] Update python-package.yml --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5de1a5f..4616b6c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Python package +name: Python Lint and Test on: push: From 675e7106fea16fed54b8fadfb595cde74a9a6701 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 20 Dec 2021 11:25:29 -0700 Subject: [PATCH 107/151] Update python-package.yml --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4616b6c..398a5ea 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -1,7 +1,7 @@ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions -name: Python Lint and Test +name: Python lint and test on: push: From b8c96e92fd37cfafa215b13ccb34c49baf3a3720 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 20 Dec 2021 11:38:29 -0700 Subject: [PATCH 108/151] Update badges for github actions --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c64b5f..d6b01a6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # svg2mod - -[![Build Status](https://travis-ci.com/svg2mod/svg2mod.svg?branch=main)](https://travis-ci.com/svg2mod/svg2mod) +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/svg2mod/svg2mod/Python%20lint%20and%20test?logo=github)](https://github.com/svg2mod/svg2mod/actions/workflows/python-package.yml) [![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod)](https://github.com/svg2mod/svg2mod/commits/main) -[![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=black)](https://pypi.org/project/svg2mod/) +[![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=purple)](https://pypi.org/project/svg2mod/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod)](https://pypi.org/project/svg2mod/) [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version)](https://pypi.org/project/svg2mod/) From 403719c59fbf8cb3abd481e7f93ba954c66de37a Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 3 Jan 2022 09:51:23 -0700 Subject: [PATCH 109/151] Fix typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6b01a6..226fef8 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ optional arguments: -F DEFAULT_FONT, --default-font DEFAULT_FONT Default font to use if the target font in a text element cannot be found - -l, --list-fonts List all fonts that can be found in common locations``` + -l, --list-fonts List all fonts that can be found in common locations ``` ## SVG Files From a29aacfb4cc37a2a243cc50a01006802500d6c87 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 3 Jan 2022 11:02:08 -0700 Subject: [PATCH 110/151] Introduce issue templates Fix typos and in workflows and add a showcase promotion for github discussions --- .github/ISSUE_TEMPLATE/bug_report.yml | 61 +++++++++++++++++++++++++++ .github/workflows/python-package.yml | 4 +- README.md | 9 +++- 3 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..74665c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,61 @@ +name: Report an issue with svg2mod +description: Report an issue with svg2mod. +labels: [bug, unconfirmed] +body: + - type: markdown + attributes: + value: | + This issue form is for reporting bugs only! + + If you have a feature or enhancement request, please use the [ideas category][fr] in the github discussions. + + [fr]: https://github.com/svg2mod/svg2mod/discussions/categories/ideas + + - type: textarea + validations: + required: true + attributes: + label: The problem + description: | + Describe the issue you are experiencing here, to communicate to the + maintainers. Tell us what you were trying to do and what happened. + + Provide a clear and concise description of what the problem is. + + - type: input + validations: + required: true + attributes: + label: Version of svg2mod + description: | + The version of svg2mod you are using. + + If installed via pip you can use `pip show svg2mod` to get the version. + + Otherwise `git describe --tag` if you installed locally. + + - type: input + validations: + required: true + id: command_line + attributes: + label: Command line + description: > + The command that when run will reproduce the error. + + - type: textarea + validations: + required: true + attributes: + label: Debug output + description: The output of the command. Please use the `--debug` flag. + render: txt + + - type: textarea + validations: + required: true + attributes: + label: Sample file and additional information + description: | + __Please attach a sample file__ and + if you have any additional information for us, use the field below. \ No newline at end of file diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 398a5ea..6b713bb 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,9 +27,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install pytest pylint pyenchant ./ + python -m pip install wheel pytest pylint pyenchant ./ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with pytlint + - name: Lint with pylint run: | pylint --rcfile .linter/cleanup.rc svg2mod setup.py - name: Test with pytest diff --git a/README.md b/README.md index 226fef8..669ab83 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ This is a program / library to convert SVG drawings to KiCad footprint module files. -It includes a modified version of [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. +It includes a modified version of [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 @@ -106,3 +106,10 @@ This supports the layers listed below. They are the same in inkscape and kicad: | B.CrtYd | -- | Yes | Note: If you have a layer "F.Cu", all of its sub-layers will be treated as "F.Cu" regardless of their names. + +## Showcase + +We'd love to see the amazing projects that use svg2mod. + +If you have a project you are proud of please post about it on our +[github discussions board](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) From 112ba79223c6c65140035788a84f088fa10ab6ed Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 3 Jan 2022 12:45:55 -0700 Subject: [PATCH 111/151] Add config.yml to issues templates --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- .github/ISSUE_TEMPLATE/config.yml | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 74665c7..d763079 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -26,7 +26,7 @@ body: validations: required: true attributes: - label: Version of svg2mod + label: Version description: | The version of svg2mod you are using. @@ -57,5 +57,5 @@ body: attributes: label: Sample file and additional information description: | - __Please attach a sample file__ and + __Please attach a sample file__ that will trigger the issue and if you have any additional information for us, use the field below. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..127775b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Feature Request + url: https://github.com/svg2mod/svg2mod/discussions/categories/ideas + about: Please use our GitHub Discussions board for making feature requests. + - name: Questions + url: https://github.com/svg2mod/svg2mod/discussions/categories/q-a + about: The issue tracker is only for issues. Please keep all other questions and discussions to GitHub Disscussions. \ No newline at end of file From 80dbafdc18ff80691699069f8fe36c7455bfcdb0 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 3 Jan 2022 16:39:38 -0700 Subject: [PATCH 112/151] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 669ab83..3e59876 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,13 @@ It includes a modified version of [cjlano's python SVG parser and drawing module ```pip install svg2mod``` +## Showcase + +We'd love to see the amazing projects that use svg2mod. + +If you have a project you are proud of please post about it on our +[github discussions board](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) + ## Example ```svg2mod -i input.svg``` @@ -106,10 +113,3 @@ This supports the layers listed below. They are the same in inkscape and kicad: | B.CrtYd | -- | Yes | Note: If you have a layer "F.Cu", all of its sub-layers will be treated as "F.Cu" regardless of their names. - -## Showcase - -We'd love to see the amazing projects that use svg2mod. - -If you have a project you are proud of please post about it on our -[github discussions board](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) From f509c296583213f27c1ccac3eccff25bdd52a32b Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 15 Jan 2022 14:57:21 -0700 Subject: [PATCH 113/151] Update badges to look better and add more --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3e59876..66cbc21 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # svg2mod -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/svg2mod/svg2mod/Python%20lint%20and%20test?logo=github)](https://github.com/svg2mod/svg2mod/actions/workflows/python-package.yml) -[![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod)](https://github.com/svg2mod/svg2mod/commits/main) +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/svg2mod/svg2mod/Python%20lint%20and%20test?logo=github&style=for-the-badge)](https://github.com/svg2mod/svg2mod/actions/workflows/python-package.yml) +[![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod?style=for-the-badge)](https://github.com/svg2mod/svg2mod/commits/main) -[![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=purple)](https://pypi.org/project/svg2mod/) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod)](https://pypi.org/project/svg2mod/) -[![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version)](https://pypi.org/project/svg2mod/) +[![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version&style=for-the-badge)](https://pypi.org/project/svg2mod/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/svg2mod?logo=python&logoColor=white&style=for-the-badge)](https://pypi.org/project/svg2mod/) + +[![GitHub Sponsors](https://img.shields.io/github/sponsors/sodium-hydrogen?logo=github&style=for-the-badge)](https://github.com/sponsors/Sodium-Hydrogen) +[![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=purple&style=for-the-badge)](https://pypi.org/project/svg2mod/) This is a program / library to convert SVG drawings to KiCad footprint module files. @@ -24,7 +27,9 @@ It includes a modified version of [cjlano's python SVG parser and drawing module We'd love to see the amazing projects that use svg2mod. If you have a project you are proud of please post about it on our -[github discussions board](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) +[github discussions board ](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) + +[![GitHub Discussions](https://img.shields.io/github/discussions/svg2mod/svg2mod?logo=github&style=for-the-badge)](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) ## Example From d7446ff1a63098a5129f6809540e7fd992431d80 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 22 Jan 2022 02:10:36 -0700 Subject: [PATCH 114/151] Add rotated arcs and a faster inlining algorithm. This also cleans some syntax, adds full rectangle support, and updates to the new year --- .gitignore | 1 + .linter/custom_dict | 1 + setup.py | 4 +- svg2mod/coloredlogger.py | 2 +- svg2mod/svg/geometry.py | 29 +++---- svg2mod/svg/svg.py | 133 +++++++++++++++---------------- svg2mod/svg2mod.py | 164 ++++++++++++++++----------------------- 7 files changed, 151 insertions(+), 183 deletions(-) diff --git a/.gitignore b/.gitignore index ed50679..1bc0947 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ svg2mod.egg-info .eggs *.kicad_mod *.mod +*.svg __pycache__ .pytest_cache Pipfile diff --git a/.linter/custom_dict b/.linter/custom_dict index 289e0ac..8d6c92c 100644 --- a/.linter/custom_dict +++ b/.linter/custom_dict @@ -43,3 +43,4 @@ poly Regex thru Faux +getLogger \ No newline at end of file diff --git a/setup.py b/setup.py index 1e1f39c..8532793 100644 --- a/setup.py +++ b/setup.py @@ -64,8 +64,8 @@ 'Natural Language :: English', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], setup_requires=setup_requirements, test_suite='test', diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index e8d6026..6216f54 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > +# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod > # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/svg2mod/svg/geometry.py b/svg2mod/svg/geometry.py index 3ad6431..7665fe8 100644 --- a/svg2mod/svg/geometry.py +++ b/svg2mod/svg/geometry.py @@ -1,5 +1,5 @@ # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > -# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > +# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod > # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -159,6 +159,11 @@ def __init__(self, arg): def __neg__(self): return Angle(Point(self.cos, -self.sin)) + def __add__(self, other): + if not isinstance(other, Angle): + try: other = Angle(other) + except: return NotImplemented + return Angle(self.angle+other.angle) class Segment: '''A segment is an object defined by 2 points''' @@ -169,7 +174,7 @@ def __init__(self, start, end): def __str__(self): return 'Segment from ' + str(self.start) + ' to ' + str(self.end) - def segments(self, precision=0): + def segments(self, __=0): ''' Segments is simply the segment start -> end''' return [self.start, self.end] @@ -191,12 +196,11 @@ def pdistance(self, p): if s.x == 0: # Vertical Segment => pdistance is the difference of abscissa return abs(self.start.x - p.x) - else: # That's 2-D perpendicular distance formula (ref: Wikipedia) - slope = s.y/s.x - # intercept: Crossing with ordinate y-axis - intercept = self.start.y - (slope * self.start.x) - return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1) + slope = s.y/s.x + # intercept: Crossing with ordinate y-axis + intercept = self.start.y - (slope * self.start.x) + return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1) def bbox(self): @@ -231,8 +235,7 @@ def control_point(self, n): '''Return Point at index n''' if n >= self.dimension: raise LookupError('Index is larger than Bezier curve dimension') - else: - return self.pts[n] + return self.pts[n] def rlength(self): '''Rough Bezier length: length of control point segments''' @@ -275,7 +278,8 @@ def segments(self, precision=0): segments.append(self._bezierN(float(t)/n)) return segments - def _bezier1(self, p0, p1, t): + @staticmethod + def _bezier1(p0, p1, t): '''Bezier curve, one dimension Compute the Point corresponding to a linear Bezier curve between p0 and p1 at "time" t ''' @@ -294,7 +298,7 @@ def _bezierN(self, t): # For each control point of nth dimension, # compute linear Bezier point a t for i in range(0,n-1): - res[i] = self._bezier1(res[i], res[i+1], t) + res[i] = Bezier._bezier1(res[i], res[i+1], t) return res[0] def transform(self, matrix): @@ -335,5 +339,4 @@ def simplify_segment(segment, epsilon): r2 = simplify_segment(segment[index:], epsilon) # Remove redundant 'middle' Point return r1[:-1] + r2 - else: - return [segment[0], segment[-1]] + return [segment[0], segment[-1]] diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 171a93e..330ec60 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -1,7 +1,7 @@ # SVG parser in Python # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > -# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > +# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod > # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -277,8 +277,7 @@ def title(self): t = self.root.find(svg_ns + 'title') if t is not None: return t - else: - return os.path.splitext(os.path.basename(self.filename))[0] + return os.path.splitext(os.path.basename(self.filename))[0] def json(self): '''Return a dictionary of children items''' @@ -381,13 +380,12 @@ def __mul__(self, other): + self.vect[5] return Matrix([a, b, c, d, e, f]) - elif isinstance(other, Point): + if isinstance(other, Point): x = other.x * self.vect[0] + other.y * self.vect[2] + self.vect[4] y = other.x * self.vect[1] + other.y * self.vect[3] + self.vect[5] return Point(x,y) - else: - return NotImplemented + return NotImplemented def __str__(self): return str(self.vect) @@ -398,6 +396,22 @@ def xlength(self, x): def ylength(self, y): '''y scale of vector''' return y * self.vect[3] + def xscale(self): + '''Return the rotated x scalar value''' + return self.vect[0]/abs(self.vect[0]) * math.sqrt(self.vect[0]**2 + self.vect[2]**2) + def yscale(self): + '''Return the rotated x scalar value''' + return self.vect[3]/abs(self.vect[3]) * math.sqrt(self.vect[1]**2 + self.vect[3]**2) + def rot(self): + '''Return the angle of rotation from the matrix. + + https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix + ''' + if self.vect[0] != 0: + return Angle(math.atan2(-self.vect[2], self.vect[0])) + if self.vect[3] != 0: + return Angle(math.atan2(self.vect[1], self.vect[3])) + return 0 @@ -639,12 +653,15 @@ def transform(self, matrix=None): else: matrix *= self.matrix self.center = matrix * self.center - self.rx = matrix.xlength(self.rx) - self.ry = matrix.ylength(self.ry) + self.rx = matrix.xscale()*self.rx + self.ry = matrix.yscale()*self.ry + self.rotation += math.degrees(matrix.rot().angle) + self.matrix= matrix def P(self, t) -> Point: - '''Return a Point on the Ellipse for t in [0..1] or % from angle 0 to the full circle''' - #TODO change point cords if rotation is set + '''Return a Point on the Ellipse for t in [0..1] or % from angle 0 to the full circle. + Rotation is not handled in this function. + ''' x = self.center.x + self.rx * math.cos(2 * math.pi * t) y = self.center.y + self.ry * math.sin(2 * math.pi * t) return Point(x,y) @@ -667,10 +684,10 @@ def segments(self, precision=0) -> List[Segment]: p.sort(key=operator.itemgetter(0)) d = Segment(p[0][1],p[1][1]).length() - ret = [x.rot(math.radians(self.rotation), x=self.center.x, y=self.center.y) for t,x in p] + ret = [x.rot(math.radians(self.rotation), x=self.center.x, y=self.center.y) for __,x in p] return [ret] - def simplify(self, precision): + def simplify(self, __): '''Return self because a 3 point representation is already simple''' return self @@ -829,6 +846,10 @@ def calcuate_center(self): elif self.sweep_flag and self.angles[1] < self.angles[0]: self.angles[1] += 2*math.pi + def transform(self, matrix=None): + super().transform(matrix) + self.end_pts[0] = self.matrix * self.end_pts[0] + self.end_pts[1] = self.matrix * self.end_pts[1] def segments(self, precision=0) -> List[Segment]: '''This returns segments as expected by the @@ -840,10 +861,10 @@ def segments(self, precision=0) -> List[Segment]: def P(self, t) -> Point: '''Return a Point on the Arc for t in [0..1] where t is the % from - the start angle to the end angle + the start angle to the end angle. + + Final angle transformation is handled in Ellipse.segments ''' - #TODO change point cords if rotation is set - # the angles are set in the calculate_center function x = self.center.x + self.rx * math.cos(((self.angles[1] - self.angles[0]) * t) + self.angles[0]) y = self.center.y + self.ry * math.sin(((self.angles[1] - self.angles[0]) * t) + self.angles[0]) return Point(x,y) @@ -867,7 +888,7 @@ def __init__(self, elt=None): def __repr__(self): return '' -class Rect(Transformable): +class Rect(Path): '''SVG tag handler This decompiles a rectangle svg xml element into essentially a path with 4 segments. @@ -882,63 +903,35 @@ class Rect(Transformable): def __init__(self, elt=None): Transformable.__init__(self, elt) if elt is not None: - self.P1 = Point(self.xlength(elt.get('x')), + self.style = elt.get('style') + p = Point(self.xlength(elt.get('x')), self.ylength(elt.get('y'))) + width = self.xlength(elt.get("width")) + height = self.xlength(elt.get("height")) + + rx = self.xlength(elt.get('rx')) + ry = self.xlength(elt.get('ry')) + if not rx: rx = ry if ry else 0 + if not ry: ry = rx if rx else 0 + if rx or ry: + cmd = f'''M{p.x+rx} {p.y} a{rx} {ry} 0 0 0 {-rx} {ry} v{height-(ry*2)} + a{rx} {ry} 0 0 0 {rx} {ry} h{width-(rx*2)} + a{rx} {ry} 0 0 0 {rx} {-ry} v{-(height-(ry*2))} + a{rx} {ry} 0 0 0 {-rx} {-ry} h{-(width-(rx*2))} z''' + else: + cmd = f'M{p.x},{p.y}v{height}h{width}v{-height}h{-width}' - self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), - self.P1.y + self.ylength(elt.get('height'))) - self.style = elt.get('style') - if (elt.get('rx') or elt.get('ry')): - logging.warning("Unsupported corner radius on rect.") + self.p = p + self.width = width + self.height = height + self.rx = rx + self.ry = ry + + self.parse(cmd) def __repr__(self): return '' - def bbox(self) -> Tuple[Point, Point]: - '''Bounding box''' - xmin = min([p.x for p in (self.P1, self.P2)]) - xmax = max([p.x for p in (self.P1, self.P2)]) - ymin = min([p.y for p in (self.P1, self.P2)]) - ymax = max([p.y for p in (self.P1, self.P2)]) - - return (Point(xmin,ymin), Point(xmax,ymax)) - - def transform(self, matrix=None): - '''Apply the provided matrix. Default (None) - If no matrix is supplied then recursively apply - it's already existing matrix to all items. - ''' - if matrix is None: - matrix = self.matrix - else: - matrix *= self.matrix - self.P1 = matrix * self.P1 - self.P2 = matrix * self.P2 - - def segments(self, precision=0) -> List[Segment]: - '''A rectangle is built with a segment going thru 4 points''' - ret = [] - Pa, Pb = Point(0,0),Point(0,0) - if self.rotation % 90 == 0: - Pa = Point(self.P1.x, self.P2.y) - Pb = Point(self.P2.x, self.P1.y) - else: - # TODO use built-in rotation function - sa = math.sin(math.radians(self.rotation)) / math.cos(math.radians(self.rotation)) - sb = -1 / sa - ba = -sa * self.P1.x + self.P1.y - bb = -sb * self.P2.x + self.P2.y - x = (ba-bb) / (sb-sa) - Pa = Point(x, sa * x + ba) - bb = -sb * self.P1.x + self.P1.y - ba = -sa * self.P2.x + self.P2.y - x = (ba-bb) / (sb-sa) - Pb = Point(x, sa * x + ba) - - ret.append([self.P1, Pa, self.P2, Pb, self.P1]) - return ret - - class Line(Transformable): '''SVG tag handler @@ -968,7 +961,7 @@ def bbox(self) -> Tuple[Point, Point]: return (Point(xmin,ymin), Point(xmax,ymax)) - def transform(self, matrix): + def transform(self, matrix=None): '''Apply the provided matrix. Default (None) If no matrix is supplied then recursively apply it's already existing matrix to all items. @@ -981,7 +974,7 @@ def transform(self, matrix): self.P2 = matrix * self.P2 self.segment = Segment(self.P1, self.P2) - def segments(self, precision=0) -> List[Segment]: + def segments(self, __=0) -> List[Segment]: '''Return the segment of the line''' return [self.segment.segments()] @@ -1140,7 +1133,7 @@ def parse(self, elt, parent): if elt.tail is not None: parent.text.append((elt.tail, parent)) - del(self.font_configs) + del self.font_configs def find_font_file(self): diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index ee270a3..41ac529 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > +# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod > # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -59,16 +59,16 @@ def main(): elif args.debug_print: logging.root.setLevel(logging.DEBUG) else: - logging.root.setLevel(logging.ERROR) + logging.root.setLevel(logging.WARNING) # Add a second logger that will bypass the log level and output anyway # It is a good practice to send only messages level INFO via this logger logging.getLogger("unfiltered").setLevel(logging.INFO) # This can be used sparingly as follows: - ''' - logging.getLogger("unfiltered").info("Message Here") - ''' + ########## + # logging.getLogger("unfiltered").info("Message Here") + ########## if args.list_fonts: fonts = svg.Text.load_system_fonts() @@ -220,6 +220,19 @@ def _orientation( p, q, r ): if val > 0: return 1 return 2 + #------------------------------------------------------------------------ + + @staticmethod + def vertical_intersection(p: svg.Point, q: svg.Point, r: float) -> svg.Point: + '''This is used for the inlining algorithm + it finds a point on a line p -> q where x = r + ''' + if p.x == q.x: + return min([p,q], key=lambda v: v.y) + if r == p.x: return p + if r == q.x: return q + return svg.Point(r, (p.y-q.y)*(r-q.x)/(p.x-q.x)+q.y) + #------------------------------------------------------------------------ @@ -244,7 +257,7 @@ def connects( self, segment: 'LineSegment' ) -> bool: #------------------------------------------------------------------------ - + def intersects( self, segment: 'LineSegment' ) -> bool: """ Return true if line segments 'p1q1' and 'p2q2' intersect. Adapted from: @@ -314,27 +327,29 @@ class PolygonSegment: identify if another polygon rests inside of the closed area of it's self. - When initializing this class it will round all - points to the specified number of decimal points, - default 10, and remove duplicate points in a row. + When initializing this class it will remove duplicate points in a row. ''' #------------------------------------------------------------------------ - def __init__( self, points:List, ndigits=10): + def __init__( self, points:List): - self.points = [points.pop(0).round(ndigits)] + self.points = [points.pop(0)] for point in points: - p = point.round(ndigits) - if self.points[-1] != p: - self.points.append(p) + if self.points[-1] != point: + self.points.append(point) if len( points ) < 3: logging.warning("Warning: Path segment has only {} points (not a polygon?)".format(len( points ))) + #------------------------------------------------------------------------ + + def _set_points(self, points: List[svg.Point]): + self.points = points[:] + #------------------------------------------------------------------------ def _find_insertion_point( self, hole: 'PolygonSegment', holes: list, other_insertions: list ): @@ -345,77 +360,29 @@ def _find_insertion_point( self, hole: 'PolygonSegment', holes: list, other_inse within any hole. ''' - connected = list( zip(*other_insertions) ) - if len(connected) > 0: - connected = [self] + list( connected[2] ) - else: - connected = [self] - - for hp in range( len(hole.points) - 1 ): - for poly in connected: - for pp in range( len(poly.points ) - 1 ): - hole_point = hole.points[hp] - bridge = LineSegment( poly.points[pp], hole_point) - trying_new_point = True - second_bridge = None - # connected_poly = poly - while trying_new_point: - trying_new_point = False - - # Check if bridge passes over other bridges that will be created - bad_point = False - for ip, insertion,_ in other_insertions: - insert = LineSegment( ip, insertion[0]) - if bridge.intersects(insert): - bad_point = True - break - - if bad_point: - continue - - # Check for intersection with each other hole: - for other_hole in holes: - - # If the other hole intersects, don't bother checking - # remaining holes: - res = other_hole.intersects( - bridge, - check_connects = ( - other_hole == hole or connected.count( other_hole ) > 0 - ), - get_points = ( - other_hole == hole or connected.count( other_hole ) > 0 - ) - ) - if isinstance(res, bool) and res: break - elif isinstance(res, tuple) and len(res) != 0: - trying_new_point = True - # connected_poly = other_hole - if other_hole == hole: - hole_point = res[0] - bridge = LineSegment( bridge.p, res[0] ) - second_bridge = LineSegment( bridge.p, res[1] ) - else: - bridge = LineSegment( res[0], hole_point ) - second_bridge = LineSegment( res[1], hole_point ) - break + highest_point = max(hole.points, key=lambda v: v.y) + vertical_line = LineSegment(highest_point, svg.Point(highest_point.x, self.bbox[1].y+1)) + intersections = {} + for h in holes: + if h == hole:continue + intersections[h] = h.intersects(vertical_line, False, count_intersections=True, get_points=True) - else: - # No other holes intersected, so this insertion point - # is acceptable: - return ( bridge.p, hole.points_starting_on_index( hole.points.index(hole_point) ), hole ) + best = [self, intersections[self][0]] + best.append(LineSegment.vertical_intersection(best[1][0], best[1][1], highest_point.x)) + for path in intersections: + for p,q in intersections[path]: + pnt = LineSegment.vertical_intersection(p, q, highest_point.x) + if pnt.y < best[2].y: + best = [path, (p,q), pnt] - if second_bridge and not trying_new_point: - bridge = second_bridge - if hole_point != bridge.q: - hole_point = bridge.q - second_bridge = None - trying_new_point = True - - logging.error("Could not insert segment without overlapping other segments") - exit(1) + if best[2] != best[1][0] and best[2] != best[1][1]: + p = best[0].points.index(best[1][0]) + q = best[0].points.index(best[1][1]) + ip = p if p < q else q + best[0]._set_points(best[0].points[:ip+1] + [best[2]] + best[0].points[ip+1:]) + return (best[2], hole, highest_point) #------------------------------------------------------------------------ @@ -450,9 +417,7 @@ def inline( self, segments: List[svg.Point] ) -> List[svg.Point]: logging.debug( " Inlining {} segments...".format( len( segments ) ) ) - segments.sort(key=lambda h: - svg.Segment(self.points[0], h.bbox[0]).length() - ) + segments.sort(reverse=True, key=lambda h: h.bbox[1].y) all_segments = segments[ : ] + [ self ] insertions = [] @@ -471,14 +436,15 @@ def inline( self, segments: List[svg.Point] ) -> List[svg.Point]: for insertion in insertions: ip = points.index(insertion[0]) + hole = insertion[1].points_starting_on_index(insertion[1].points.index(insertion[2])) if ( - points[ ip ].x == insertion[ 1 ][ 0 ].x and - points[ ip ].y == insertion[ 1 ][ 0 ].y + points[ ip ].x == hole[ 0 ].x and + points[ ip ].y == hole[ 0 ].y ): - points = points[:ip+1] + insertion[ 1 ][ 1 : -1 ] + points[ip:] + points = points[:ip+1] + hole[ 1 : -1 ] + points[ip:] else: - points = points[:ip+1] + insertion[ 1 ] + points[ip:] + points = points[:ip+1] + hole + points[ip:] return points @@ -496,12 +462,13 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int with the polygon. get_points returns a tuple of the line that intersects - with line_segment + with line_segment. count_intersection in combination will + return a list of tuples of line segments. ''' hole_segment = LineSegment() - intersections = 0 + intersections = [] if get_points else 0 # Check each segment of other hole for intersection: for point in self.points: @@ -516,9 +483,12 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int if line_segment.intersects( hole_segment ): if count_intersections: + if get_points: + intersections.append((hole_segment.p, hole_segment.q)) + else: + intersections += 1 # If line_segment passes through a point this prevents a second false positive hole_segment.q = None - intersections += 1 elif get_points: return hole_segment.p, hole_segment.q else: @@ -603,7 +573,7 @@ def are_distinct(self, polygon): # Check number of horizontal intersections. If the number is odd then it the smaller polygon # is contained. If the number is even then the polygon is outside of the larger polygon if not distinct: - tline = LineSegment(smaller.points[0], svg.Point(larger.bbox[1].x, smaller.points[0].y)) + tline = LineSegment(smaller.points[0], svg.Point(larger.bbox[1].x+1, smaller.points[0].y)) distinct = bool((larger.intersects(tline, False, True) + 1)%2) return distinct @@ -635,7 +605,7 @@ def _prune_hidden( self, items = None ): elif item.name != "": self.svg.items.append( item ) - if(item.items): + if item.items: self._prune_hidden( item.items ) def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", ignore_hidden_layers=False ): @@ -649,7 +619,7 @@ def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", self.svg = svg.parse( file_name) logging.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) - if( ignore_hidden_layers ): + if ignore_hidden_layers: self._prune_hidden() @@ -821,7 +791,7 @@ def _calculate_translation( self ): min_point, max_point = self.imported.svg.bbox() - if(self.center): + 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 @@ -878,7 +848,7 @@ def _write_items( self, items, layer, flip = False ): self._write_items( item.items, layer, flip ) continue - elif isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text)): + if isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text)): segments = [ PolygonSegment( segment ) @@ -923,7 +893,7 @@ def _write_items( self, items, layer, flip = False ): ) continue - elif len( segments ) > 0: + if len( segments ) > 0: points = segments[ 0 ].points if len ( segments ) == 1: From 3806f831c275905982de34b7d27f23e4f3592613 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Sat, 22 Jan 2022 03:21:10 -0700 Subject: [PATCH 115/151] Fix some issues with distinct detection --- svg2mod/svg/svg.py | 11 ++++++++++- svg2mod/svg2mod.py | 28 ++++++++++++++++++---------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 330ec60..86f7bd6 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -62,6 +62,11 @@ '%' : 1 / 100.0 # 1 percent } +# Logging spammed 'Unable to find font because no font was specified.' +# this allows it to only print the error once before muting it for that run. +_font_warning_sent = False + + class Transformable: '''Abstract class for objects that can be geometrically drawn & transformed''' def __init__(self, elt=None): @@ -1150,7 +1155,10 @@ def find_font_file(self): ''' if self.font_family is None: if Text.default_font is None: - logging.error("Unable to find font because no font was specified.") + global _font_warning_sent + if not _font_warning_sent: + logging.error("Unable to find font because no font was specified.") + _font_warning_sent = True return None self.font_family = Text.default_font fonts = [fnt.strip().strip("'") for fnt in self.font_family.split(",")] @@ -1360,6 +1368,7 @@ def default(self, obj): # Make fontTools more quiet loggingTools.configLogger(level=logging.INFO) + # SVG tag handler classes are initialized here # (classes must be defined before) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 41ac529..adaf9eb 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -224,14 +224,14 @@ def _orientation( p, q, r ): @staticmethod def vertical_intersection(p: svg.Point, q: svg.Point, r: float) -> svg.Point: - '''This is used for the inlining algorithm + '''This is used for the in-lining algorithm it finds a point on a line p -> q where x = r ''' if p.x == q.x: return min([p,q], key=lambda v: v.y) if r == p.x: return p if r == q.x: return q - return svg.Point(r, (p.y-q.y)*(r-q.x)/(p.x-q.x)+q.y) + return svg.Point(r, (p.y-q.y)*(r-q.x)/(p.x-q.x)+q.y) #------------------------------------------------------------------------ @@ -255,9 +255,17 @@ def connects( self, segment: 'LineSegment' ) -> bool: if self.p == segment.q: return True return False + #------------------------------------------------------------------------ + + def on_line(self, point: svg.Point) -> bool: + '''Returns true if the point is on the line. + Adapted from: + https://stackoverflow.com/questions/36487156/javascript-determine-if-a-point-resides-above-or-below-a-line-defined-by-two-poi + ''' + return not (self.p.x-self.q.x)*(point.y-self.q.y) - (self.p.y-self.q.y)*(point.x-self.q.x) #------------------------------------------------------------------------ - + def intersects( self, segment: 'LineSegment' ) -> bool: """ Return true if line segments 'p1q1' and 'p2q2' intersect. Adapted from: @@ -346,7 +354,7 @@ def __init__( self, points:List): #------------------------------------------------------------------------ - + def _set_points(self, points: List[svg.Point]): self.points = points[:] @@ -468,7 +476,8 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int hole_segment = LineSegment() - intersections = [] if get_points else 0 + intersections = 0 + intersect_segs = [] # Check each segment of other hole for intersection: for point in self.points: @@ -484,18 +493,17 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int if count_intersections: if get_points: - intersections.append((hole_segment.p, hole_segment.q)) + intersect_segs.append((hole_segment.p, hole_segment.q)) else: - intersections += 1 - # If line_segment passes through a point this prevents a second false positive - hole_segment.q = None + # If line_segment passes through a point this prevents a second false positive + intersections += 0 if line_segment.on_line(hole_segment.q) else 1 elif get_points: return hole_segment.p, hole_segment.q else: return True if count_intersections: - return intersections + return intersect_segs if get_points else intersections if get_points and not check_connects: return () return False From 576747ed81a6dbe1c8ba63a38f65086cc19360a3 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 28 Jan 2022 13:29:39 -0700 Subject: [PATCH 116/151] Added keepout, drill, and layer options Split svg2mod/svg2mod into four files: cli, exporter, importer, svg2mod --- .linter/cleanup.rc | 1 - .linter/custom_dict | 3 +- README.md | 95 ++- setup.py | 6 +- svg2mod/__init__.py | 23 + svg2mod/cli.py | 340 +++++++++ svg2mod/coloredlogger.py | 10 +- svg2mod/exporter.py | 1280 +++++++++++++++++++++++++++++++++ svg2mod/importer.py | 71 ++ svg2mod/svg/svg.py | 96 ++- svg2mod/svg2mod.py | 1448 +------------------------------------- 11 files changed, 1872 insertions(+), 1501 deletions(-) create mode 100644 svg2mod/cli.py create mode 100644 svg2mod/exporter.py create mode 100644 svg2mod/importer.py diff --git a/.linter/cleanup.rc b/.linter/cleanup.rc index 379662d..e74034d 100644 --- a/.linter/cleanup.rc +++ b/.linter/cleanup.rc @@ -75,7 +75,6 @@ enable=c-extension-no-member, no-else-continue, no-else-raise, useless-object-inheritance, - too-few-public-methods, too-many-arguments, too-many-branches, too-many-instance-attributes, diff --git a/.linter/custom_dict b/.linter/custom_dict index 8d6c92c..dab75d0 100644 --- a/.linter/custom_dict +++ b/.linter/custom_dict @@ -43,4 +43,5 @@ poly Regex thru Faux -getLogger \ No newline at end of file +getLogger +keepout \ No newline at end of file diff --git a/README.md b/README.md index 66cbc21..45c6bd8 100644 --- a/README.md +++ b/README.md @@ -95,26 +95,75 @@ svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain This supports the layers listed below. They are the same in inkscape and kicad: -| KiCad layer(s) | KiCad legacy | KiCad pretty | -|:----------------:|:------------:|:------------:| -| F.Cu | Yes | Yes | -| B.Cu | Yes | Yes | -| F.Adhes | Yes | Yes | -| B.Adhes | Yes | Yes | -| F.Paste | Yes | Yes | -| B.Paste | Yes | Yes | -| F.SilkS | Yes | Yes | -| B.SilkS | Yes | Yes | -| F.Mask | Yes | Yes | -| B.Mask | Yes | Yes | -| Dwgs.User | Yes | Yes | -| Cmts.User | Yes | Yes | -| Eco1.User | Yes | Yes | -| Eco2.User | Yes | Yes | -| Edge.Cuts | Yes | Yes | -| F.Fab | -- | Yes | -| B.Fab | -- | Yes | -| F.CrtYd | -- | Yes | -| B.CrtYd | -- | Yes | - -Note: If you have a layer "F.Cu", all of its sub-layers will be treated as "F.Cu" regardless of their names. +| KiCad layer(s) | KiCad legacy | KiCad pretty | +|:----------------------:|:------------:|:------------:| +| F.Cu [^1] | Yes | Yes | +| B.Cu [^1] | Yes | Yes | +| F.Adhes | Yes | Yes | +| B.Adhes | Yes | Yes | +| F.Paste | Yes | Yes | +| B.Paste | Yes | Yes | +| F.SilkS | Yes | Yes | +| B.SilkS | Yes | Yes | +| F.Mask | Yes | Yes | +| B.Mask | Yes | Yes | +| Dwgs.User | Yes | Yes | +| Cmts.User | Yes | Yes | +| Eco1.User | Yes | Yes | +| Eco2.User | Yes | Yes | +| Edge.Cuts | Yes | Yes | +| F.Fab | -- | Yes | +| B.Fab | -- | Yes | +| F.CrtYd | -- | Yes | +| B.CrtYd | -- | Yes | +| Drill.Cu [^1] | -- | Yes | +| Drill.Mech [^1] | -- | Yes | +| *.Keepout [^1][^2][^3] | -- | Yes | + +Note: If you have a layer `F.Cu`, all of its sub-layers will be treated as `F.Cu` regardless of their names. + +### Layer Options + +Some layers can have options when saving to the newer 'pretty' format. + +The options are seperated from the layer name by `:`. Ex `F.Cu:...` + +Some options can have arguments which are also seperated from +the option key by `:`. If an option has more than one argument they +are seperated by a comma. Ex: `F.Cu:Pad:1,mask`. + +If a layer has more than one option they will be seperated by `;` +Ex: `F.Cu:pad;...` + +Supported Arguments: + +* Pad + + Any copper layer can have the pad specified. + The pad option can be used solo (`F.Cu:Pad`) or it can also have it's own arguments. + The arguments are: + + * Number + If it is set it will specify the number of the pad. Ex: `Pad:1` + + * Paste _(Not avalable for `Drill.Cu`)_ + * Mask _(Not avalable for `Drill.Cu`)_ + + +* Allowed + + Keepout areas will prevent anything from being placed inside them. + To allow some things to be placed inside the keepout zone a comma + seperated list of any of the following options can be used: + `tracks`,`vias`,`pads`,`copperpour`,`footprints` + + + + +[^1]: These layers can have arguments when svg2mod is in pretty mode + +[^2]: Only works in Kicad versions >= v6. + +[^3]: The * can be { *, F, B, I } or any combination like FB or BI. These options are for Front, Back, and Internal. + + diff --git a/setup.py b/setup.py index 8532793..df03109 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ author_email='', url='https://github.com/svg2mod/svg2mod', packages=setuptools.find_packages(), - entry_points={'console_scripts':['svg2mod = svg2mod.svg2mod:main']}, + entry_points={'console_scripts':['svg2mod = svg2mod.cli:main']}, package_dir={'svg2mod':'svg2mod'}, include_package_data=True, package_data={'kipart': ['*.gif', '*.png']}, @@ -58,8 +58,10 @@ zip_safe=False, keywords='svg2mod, KiCAD, inkscape', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Science/Research', + 'Intended Audience :: Developers', + 'Intended Audience :: Manufacturing', 'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', 'Natural Language :: English', 'Programming Language :: Python :: 3', diff --git a/svg2mod/__init__.py b/svg2mod/__init__.py index e69de29..7b3e7ec 100644 --- a/svg2mod/__init__.py +++ b/svg2mod/__init__.py @@ -0,0 +1,23 @@ +# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod > + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +''' +This module contains the necessary tools to convert from +the svg objects provided from the svg2mod.svg module to +KiCad file formats. +This currently supports both the pretty format and +the legacy mod format. +''' \ No newline at end of file diff --git a/svg2mod/cli.py b/svg2mod/cli.py new file mode 100644 index 0000000..552d603 --- /dev/null +++ b/svg2mod/cli.py @@ -0,0 +1,340 @@ +# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod > + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +''' +The command line interface for using svg2mod +as a tool in a terminal. +''' + +import argparse +import logging +import os +import shlex +import sys +import traceback + +import svg2mod.coloredlogger as coloredlogger +from svg2mod import svg +from svg2mod.exporter import (DEFAULT_DPI, Svg2ModExportLegacy, + Svg2ModExportLegacyUpdater, Svg2ModExportPretty) +from svg2mod.importer import Svg2ModImport + + +def main(): + '''This function handles the scripting package calls. + It is setup to read the arguments from `get_arguments()` + then parse the target svg file and output all converted + objects into a kicad footprint module. + ''' + + args,_ = get_arguments() + + + # Setup root logger to use terminal colored outputs as well as stdout and stderr + coloredlogger.split_logger(logging.root) + + if args.verbose_print: + logging.root.setLevel(logging.INFO) + elif args.debug_print: + logging.root.setLevel(logging.DEBUG) + else: + logging.root.setLevel(logging.WARNING) + + # Add a second logger that will bypass the log level and output anyway + # It is a good practice to send only messages level INFO via this logger + logging.getLogger("unfiltered").setLevel(logging.INFO) + + # This can be used sparingly as follows: + ########## + # logging.getLogger("unfiltered").info("Message Here") + ########## + + if args.list_fonts: + fonts = svg.Text.load_system_fonts() + logging.getLogger("unfiltered").info("Font Name: list of supported styles.") + for font in fonts: + fnt_text = f" {font}:" + for styles in fonts[font]: + fnt_text += f" {styles}," + fnt_text = fnt_text.strip(",") + logging.getLogger("unfiltered").info(fnt_text) + sys.exit(0) + if args.default_font: + svg.Text.default_font = args.default_font + + pretty = args.format == 'pretty' + use_mm = args.units == 'mm' + + if pretty: + + if not use_mm: + logging.critical("Error: decimal units only allowed with legacy output type") + sys.exit( -1 ) + + #if args.include_reverse: + #print( + #"Warning: reverse footprint not supported or required for" + + #" pretty output format" + #) + + try: + # Import the SVG: + imported = Svg2ModImport( + args.input_file_name, + args.module_name, + args.module_value, + args.ignore_hidden_layers, + ) + + # Pick an output file name if none was provided: + if args.output_file_name is None: + + args.output_file_name = os.path.splitext( + os.path.basename( args.input_file_name ) + )[ 0 ] + + # Append the correct file name extension if needed: + if pretty: + extension = ".kicad_mod" + else: + extension = ".mod" + if args.output_file_name[ - len( extension ) : ] != extension: + args.output_file_name += extension + + # Create an exporter: + if pretty: + exported = Svg2ModExportPretty( + imported, + args.output_file_name, + args.center, + args.scale_factor, + args.precision, + dpi = args.dpi, + pads = args.convert_to_pads, + ) + + else: + + # If the module file exists, try to read it: + exported = None + if os.path.isfile( args.output_file_name ): + + try: + exported = Svg2ModExportLegacyUpdater( + imported, + args.output_file_name, + args.center, + args.scale_factor, + args.precision, + args.dpi, + ) + + except Exception as e: + raise e + #print( e.message ) + #exported = None + + # Write the module file: + if exported is None: + exported = Svg2ModExportLegacy( + imported, + args.output_file_name, + args.center, + args.scale_factor, + args.precision, + use_mm = use_mm, + dpi = args.dpi, + ) + + cmd_args = [os.path.basename(sys.argv[0])] + sys.argv[1:] + cmdline = ' '.join(shlex.quote(x) for x in cmd_args) + + # Export the footprint: + exported.write(cmdline) + except Exception as e: + if args.debug_print: + traceback.print_exc() + else: + logging.critical(f'Unhandled exception (Exiting)\n {type(e).__name__}: {e} ') + exit(-1) + +#---------------------------------------------------------------------------- + +def get_arguments(): + ''' Return an instance of pythons argument parser + with all the command line functionalities arguments + ''' + + parser = argparse.ArgumentParser( + description = ( + 'Convert Inkscape SVG drawings to KiCad footprint modules.' + ) + ) + + mux = parser.add_mutually_exclusive_group(required=True) + + mux.add_argument( + '-i', '--input-file', + type = str, + dest = 'input_file_name', + metavar = 'FILENAME', + help = "Name of the SVG file", + ) + + parser.add_argument( + '-o', '--output-file', + type = str, + dest = 'output_file_name', + metavar = 'FILENAME', + help = "Name of the module file", + ) + + parser.add_argument( + '-c', '--center', + dest = 'center', + action = 'store_const', + const = True, + help = "Center the module to the center of the bounding box", + default = False, + ) + + parser.add_argument( + '-P', '--convert-pads', + dest = 'convert_to_pads', + action = 'store_const', + const = True, + help = "Convert any artwork on Cu layers to pads", + default = False, + ) + + parser.add_argument( + '-v', '--verbose', + dest = 'verbose_print', + action = 'store_const', + const = True, + help = "Print more verbose messages", + default = False, + ) + + parser.add_argument( + '--debug', + dest = 'debug_print', + action = 'store_const', + const = True, + help = "Print debug level messages", + default = False, + ) + + parser.add_argument( + '-x', '--exclude-hidden', + dest = 'ignore_hidden_layers', + action = 'store_const', + const = True, + help = "Do not export hidden layers", + default = False, + ) + + parser.add_argument( + '-d', '--dpi', + type = int, + dest = 'dpi', + metavar = 'DPI', + help = "DPI of the SVG file (int)", + default = DEFAULT_DPI, + ) + + parser.add_argument( + '-f', '--factor', + type = float, + dest = 'scale_factor', + metavar = 'FACTOR', + help = "Scale paths by this factor", + default = 1.0, + ) + + parser.add_argument( + '-p', '--precision', + type = float, + dest = 'precision', + metavar = 'PRECISION', + help = "Smoothness for approximating curves with line segments. Input is the approximate length for each line segment in SVG pixels (float)", + default = 10.0, + ) + parser.add_argument( + '--format', + type = str, + dest = 'format', + metavar = 'FORMAT', + choices = [ 'legacy', 'pretty' ], + help = "Output module file format (legacy|pretty)", + default = 'pretty', + ) + + parser.add_argument( + '--name', '--module-name', + type = str, + dest = 'module_name', + metavar = 'NAME', + help = "Base name of the module", + default = "svg2mod", + ) + + parser.add_argument( + '--units', + type = str, + dest = 'units', + metavar = 'UNITS', + choices = [ 'decimal', 'mm' ], + help = "Output units, if output format is legacy (decimal|mm)", + default = 'mm', + ) + + parser.add_argument( + '--value', '--module-value', + type = str, + dest = 'module_value', + metavar = 'VALUE', + help = "Value of the module", + default = "G***", + ) + + parser.add_argument( + '-F', '--default-font', + type = str, + dest = 'default_font', + help = "Default font to use if the target font in a text element cannot be found", + ) + + mux.add_argument( + '-l', '--list-fonts', + dest = 'list_fonts', + const = True, + default = False, + action = "store_const", + help = "List all fonts that can be found in common locations", + ) + + return parser.parse_args(), parser + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +if __name__ == "__main__": + main() + + +#---------------------------------------------------------------------------- diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index 6216f54..5a588c4 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -18,8 +18,8 @@ ANSI color codes based on the logged message's level ''' -import sys import logging +import sys class Formatter(logging.Formatter): @@ -44,10 +44,12 @@ def format(self, record): color, sends the message to the super.format, and finally returns the style to the original format ''' - fmt_org = self._style._fmt - self._style._fmt = Formatter.color[record.levelno] + fmt_org + Formatter.reset + if sys.stdout.isatty(): + fmt_org = self._style._fmt + self._style._fmt = Formatter.color[record.levelno] + fmt_org + Formatter.reset result = logging.Formatter.format(self, record) - self._style._fmt = fmt_org + if sys.stdout.isatty(): + self._style._fmt = fmt_org return result def split_logger(logger, formatter=Formatter(), brkpoint=logging.WARNING): diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py new file mode 100644 index 0000000..a09cbb1 --- /dev/null +++ b/svg2mod/exporter.py @@ -0,0 +1,1280 @@ +# Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod > + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +''' +Tools to convert data from Svg2ModImport to +the file information used in kicad module files. +''' + + +import datetime +import io +import json +import logging +import os +import re +import time +from abc import ABC, abstractmethod + +from svg2mod import svg +from svg2mod.importer import Svg2ModImport +from svg2mod.svg2mod import PolygonSegment + +#---------------------------------------------------------------------------- + +DEFAULT_DPI = 96 # 96 as of Inkscape 0.92 + +#---------------------------------------------------------------------------- + +class Svg2ModExport(ABC): + ''' An abstract class to provide functionality + to write to kicad module file. + The abstract methods are the file type specific + example: pretty, legacy + ''' + + @property + @abstractmethod + def layer_map(self ): + ''' This should be overwritten by a dictionary object of layer maps ''' + pass + + @abstractmethod + def _get_layer_name( self, item_name, name, front ):pass + + @abstractmethod + def _write_library_intro( self, cmdline ): pass + + @abstractmethod + def _get_module_name( self, front = None ): pass + + @abstractmethod + def _write_module_header( self, label_size, label_pen, reference_y, value_y, front,): pass + + @abstractmethod + def _write_modules( self ): pass + + @abstractmethod + def _write_module_footer( self, front ):pass + + @abstractmethod + def _write_polygon_header( self, points, layer ):pass + + @abstractmethod + def _write_polygon( self, points, layer, fill, stroke, stroke_width ):pass + + @abstractmethod + def _write_polygon_footer( self, layer, stroke_width ):pass + + @abstractmethod + def _write_polygon_point( self, point ):pass + + @abstractmethod + def _write_polygon_segment( self, p, q, layer, stroke_width ):pass + + @abstractmethod + def _write_thru_hole( self, circle, layer ):pass + + #------------------------------------------------------------------------ + + @staticmethod + def _convert_decimal_to_mm( decimal ): + return float( decimal ) * 0.00254 + + + #------------------------------------------------------------------------ + + @staticmethod + def _convert_mm_to_decimal( mm ): + return int( round( mm * 393.700787 ) ) + + + #------------------------------------------------------------------------ + + def _get_fill_stroke( self, item ): + + s = item.style + + fill = False if s.get('fill') and s["fill"] == "none" else True + fill = fill if not s.get('fill-opacity') or float(s['fill-opacity']) != 0 else False + + stroke = False if s.get('stroke') and s["stroke"] == "none" else True + stroke = stroke if not s.get('stroke-opacity') or float(s['stroke-opacity']) != 0 else False + + stroke_width = s["stroke-width"] if stroke and s.get('stroke-width') else None + + if stroke_width: + stroke_width *= self.scale_factor + + if stroke and stroke_width is None: + # Give a default stroke width? + stroke_width = self._convert_decimal_to_mm( 1 ) if self.use_mm else 1 + + + + return fill, stroke, stroke_width + + + #------------------------------------------------------------------------ + + def __init__( + self, + svg2mod_import = Svg2ModImport(), + file_name = None, + center = False, + scale_factor = 1.0, + precision = 20.0, + use_mm = True, + dpi = DEFAULT_DPI, + pads = False, + ): + if use_mm: + # 25.4 mm/in; + scale_factor *= 25.4 / float(dpi) + use_mm = True + else: + # PCBNew uses decimal (10K DPI); + scale_factor *= 10000.0 / float(dpi) + + 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 + self.dpi = dpi + self.convert_pads = pads + + # Local instance variables + self.translation = None + self.layers = {} + self.output_file = None + self.raw_file_data = None + + + #------------------------------------------------------------------------ + + def add_svg_element(self, elem : svg.Transformable, layer="F.SilkS"): + ''' This can be used to add a svg element + to a specific layer. + If the importer doesn't have a svg element + it will also create an empty Svg object. + ''' + grp = svg.Group() + grp.name = layer + grp.items.append(elem) + try: + self.imported.svg.items.append(grp) + except AttributeError: + self.imported.svg = svg.Svg() + self.imported.svg.items.append(grp) + + #------------------------------------------------------------------------ + + + def _calculate_translation( self ): + + min_point, max_point = self.imported.svg.bbox() + + 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, + ) + + else: + self.translation = svg.Point( + 0.0, + 0.0, + ) + + #------------------------------------------------------------------------ + + def _prune( self, items = None ): + '''Find and keep only the layers of interest.''' + + if items is None: + + self.layers = {} + for name in self.layer_map.keys(): + self.layers[ name ] = [] + + items = self.imported.svg.items + self.imported.svg.items = [] + + kept_layers = {} + + for item in items: + + if not isinstance( item, svg.Group ): + continue + i_name = item.name.split(":", 1) + + for name in self.layers.keys(): + # if name == i_name[0] and i_name[0] != "": + if re.match( f'^{name}$', i_name[0]): + if kept_layers.get(i_name[0]): + kept_layers[i_name[0]].append(item.name) + else: + kept_layers[i_name[0]] = [item.name] + #logging.getLogger("unfiltered").info( "Found SVG layer: {}".format( item.name ) ) + + self.imported.svg.items.append( item ) + self.layers[name].append((i_name, item)) + break + else: + self._prune( item.items ) + + for kept in sorted(kept_layers.keys()): + logging.getLogger("unfiltered").info( "Found SVG layer: {}".format( kept ) ) + logging.debug( " Detailed SVG layers: {}".format( ", ".join(kept_layers[kept]) ) ) + + + #------------------------------------------------------------------------ + + def _write_items( self, items, layer, flip = False ): + + for item in items: + + if isinstance( item, svg.Group ): + self._write_items( item.items, layer, flip ) + continue + + if re.match(r"^Drill.\w+", layer): + if isinstance(item, svg.Circle): + self._write_thru_hole(item, layer) + else: + logging.warning( "Non Circle SVG element in drill layer: {}".format(item.__class__.__name__)) + + elif isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text)): + + segments = [ + PolygonSegment( segment ) + for segment in item.segments( + precision = self.precision + ) + ] + + fill, stroke, stroke_width = self._get_fill_stroke( item ) + fill = (False if layer == "Edge.Cuts" else fill) + + fill = (True if re.match("^Keepout", layer) else fill) + stroke_width = (0.508 if re.match("^Keepout", layer) else stroke_width) + + for segment in segments: + segment.process( self, flip, fill ) + + if len( segments ) > 1: + segments.sort(key=lambda v: svg.Segment(v.bbox[0], v.bbox[1]).length(), reverse=True) + + while len(segments) > 0: + inlinable = [segments[0]] + for seg in segments[1:]: + if not inlinable[0].are_distinct(seg): + append = True + if len(inlinable) > 1: + for hole in inlinable[1:]: + if not hole.are_distinct(seg): + append = False + break + if append: inlinable.append(seg) + for poly in inlinable: + segments.pop(segments.index(poly)) + if len(inlinable) > 1: + points = inlinable[ 0 ].inline( inlinable[ 1 : ] ) + elif len(inlinable) > 0: + points = inlinable[ 0 ].points + + logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) + + self._write_polygon( + points, layer, fill, stroke, stroke_width + ) + continue + + if len( segments ) > 0: + points = segments[ 0 ].points + + if len ( segments ) == 1: + + logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) + + self._write_polygon( + points, layer, fill, stroke, stroke_width + ) + else: + logging.info( " Skipping {} with 0 points".format(item.__class__.__name__)) + + else: + logging.warning( "Unsupported SVG element: {}".format(item.__class__.__name__)) + + + #------------------------------------------------------------------------ + + def _write_module( self, front ): + + module_name = self._get_module_name( front ) + + min_point, max_point = self.imported.svg.bbox() + min_point = self.transform_point( min_point, flip = False ) + max_point = self.transform_point( max_point, flip = False ) + + label_offset = 1200 + label_size = 600 + label_pen = 120 + + if self.use_mm: + label_size = self._convert_decimal_to_mm( label_size ) + label_pen = self._convert_decimal_to_mm( label_pen ) + reference_y = min_point.y - self._convert_decimal_to_mm( label_offset ) + value_y = max_point.y + self._convert_decimal_to_mm( label_offset ) + else: + reference_y = min_point.y - label_offset + value_y = max_point.y + label_offset + + self._write_module_header( + label_size, label_pen, + reference_y, value_y, + front, + ) + + for name, groups in self.layers.items(): + for i_name, group in groups: + + if group is None: continue + + layer = self._get_layer_name( i_name, name, front ) + + #print( " Writing layer: {}".format( name ) ) + self._write_items( group.items, layer, not front ) + + self._write_module_footer( front ) + + + #------------------------------------------------------------------------ + + def _write_polygon_filled( self, points, layer, stroke_width = 0.0 ): + + self._write_polygon_header( points, layer ) + + for point in points: + self._write_polygon_point( point ) + + self._write_polygon_footer( layer, stroke_width ) + + + #------------------------------------------------------------------------ + + def _write_polygon_outline( self, points, layer, stroke_width ): + + prior_point = None + for point in points: + + if prior_point is not None: + + self._write_polygon_segment( + prior_point, point, layer, stroke_width + ) + + prior_point = point + + + #------------------------------------------------------------------------ + + def transform_point( self, point, flip = False ): + ''' Transform provided point by this + classes scale factor. + ''' + + transformed_point = svg.Point( + ( point.x + self.translation.x ) * self.scale_factor, + ( point.y + self.translation.y ) * self.scale_factor, + ) + + if flip: + transformed_point.x *= -1 + + if self.use_mm: + transformed_point.x = transformed_point.x + transformed_point.y = transformed_point.y + else: + transformed_point.x = int( round( transformed_point.x ) ) + transformed_point.y = int( round( transformed_point.y ) ) + + return transformed_point + + + #------------------------------------------------------------------------ + + def write( self, cmdline="scripting" ): + '''Write the kicad footprint file. + The value from the command line argument + is set in a comment in the header of the file. + + If self.file_name is not null then this will + overwrite the target file with the data provided. + However if it is null then all data is written + to the string IO class (for same API as writing) + then dumped into self.raw_file_data before the + writer is closed. + ''' + + self._prune() + + # Must come after pruning: + self._calculate_translation() + + if self.file_name: + logging.getLogger("unfiltered").info( "Writing module file: {}".format( self.file_name ) ) + self.output_file = open( self.file_name, 'w' ) + else: + self.output_file = io.StringIO() + + self._write_library_intro(cmdline) + + self._write_modules() + + if self.file_name is None: + self.raw_file_data = self.output_file.getvalue() + + self.output_file.close() + self.output_file = None + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +class Svg2ModExportLegacy( Svg2ModExport ): + ''' A child of Svg2ModExport that implements + specific functionality for kicad legacy file types + ''' + + layer_map = { + #'inkscape-name' : [ kicad-front, kicad-back ], + '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 ], + 'Eco2.User' : [ 27, 27 ], + 'Edge.Cuts' : [ 28, 28 ], + } + + + #------------------------------------------------------------------------ + + def __init__( + self, + svg2mod_import, + file_name, + center, + scale_factor = 1.0, + precision = 20.0, + use_mm = True, + dpi = DEFAULT_DPI, + ): + super( Svg2ModExportLegacy, self ).__init__( + svg2mod_import, + file_name, + center, + scale_factor, + precision, + use_mm, + dpi, + pads = False, + ) + + self.include_reverse = True + + + #------------------------------------------------------------------------ + + def _get_layer_name( self, item_name, name, front ): + + layer_info = self.layer_map[ name ] + layer = layer_info[ 0 ] + if not front and layer_info[ 1 ] is not None: + layer = layer_info[ 1 ] + + return layer + + + #------------------------------------------------------------------------ + + def _get_module_name( self, front = None ): + + if self.include_reverse and not front: + return self.imported.module_name + "-rev" + + return self.imported.module_name + + + #------------------------------------------------------------------------ + + def _write_library_intro( self, cmdline ): + + modules_list = self._get_module_name( front = True ) + if self.include_reverse: + modules_list += ( + "\n" + + self._get_module_name( front = False ) + ) + + units = "" + if self.use_mm: + units = "\nUnits mm" + + self.output_file.write( """PCBNEW-LibModule-V1 {0}{1} +$INDEX +{2} +$EndINDEX +# +# Converted using: {3} +# +""".format( + datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ), + units, + modules_list, + cmdline.replace("\\","\\\\") + ) + ) + + + #------------------------------------------------------------------------ + + def _write_module_header( + self, label_size, label_pen, + reference_y, value_y, front, + ): + + self.output_file.write( """$MODULE {0} +Po 0 0 0 {6} 00000000 00000000 ~~ +Li {0} +T0 0 {1} {2} {2} 0 {3} N I 21 "{0}" +T1 0 {5} {2} {2} 0 {3} N I 21 "{4}" +""".format( + self._get_module_name( front ), + reference_y, + label_size, + label_pen, + self.imported.module_value, + value_y, + 15, # Seems necessary + ) + ) + + + #------------------------------------------------------------------------ + + def _write_module_footer( self, front ): + + self.output_file.write( + "$EndMODULE {0}\n".format( self._get_module_name( front ) ) + ) + + + #------------------------------------------------------------------------ + + def _write_modules( self ): + + self._write_module( front = True ) + + if self.include_reverse: + self._write_module( front = False ) + + self.output_file.write( "$EndLIBRARY" ) + + + #------------------------------------------------------------------------ + + def _write_polygon( self, points, layer, fill, stroke, stroke_width ): + + if fill: + self._write_polygon_filled( + points, layer + ) + + if stroke: + + self._write_polygon_outline( + points, layer, stroke_width + ) + + + #------------------------------------------------------------------------ + + def _write_polygon_footer( self, layer, stroke_width ): + + pass + + + #------------------------------------------------------------------------ + + def _write_polygon_header( self, points, layer ): + + pen = 1 + if self.use_mm: + pen = self._convert_decimal_to_mm( pen ) + + self.output_file.write( "DP 0 0 0 0 {} {} {}\n".format( + len( points ), + pen, + layer + ) ) + + + #------------------------------------------------------------------------ + + def _write_polygon_point( self, point ): + + self.output_file.write( + "Dl {} {}\n".format( point.x, point.y ) + ) + + + #------------------------------------------------------------------------ + + def _write_polygon_segment( self, p, q, layer, stroke_width ): + + self.output_file.write( "DS {} {} {} {} {} {}\n".format( + p.x, p.y, + q.x, q.y, + stroke_width, + layer + ) ) + + + #------------------------------------------------------------------------ + + def _write_thru_hole( self, circle, layer ): + + pass + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +class Svg2ModExportLegacyUpdater( Svg2ModExportLegacy ): + ''' A Svg2Mod exporter class that reads some settings + from an already existing module and will append its + changes to the file. + ''' + + #------------------------------------------------------------------------ + + def __init__( + self, + svg2mod_import, + file_name, + center, + scale_factor = 1.0, + precision = 20.0, + dpi = DEFAULT_DPI, + include_reverse = True, + ): + self.file_name = file_name + use_mm = self._parse_output_file() + + super( Svg2ModExportLegacyUpdater, self ).__init__( + svg2mod_import, + file_name, + center, + scale_factor, + precision, + use_mm, + dpi, + ) + + + #------------------------------------------------------------------------ + + def _parse_output_file( self ): + + logging.info( "Parsing module file: {}".format( self.file_name ) ) + module_file = open( self.file_name, 'r' ) + lines = module_file.readlines() + module_file.close() + + self.loaded_modules = {} + self.post_index = [] + self.pre_index = [] + use_mm = False + + index = 0 + + # Find the start of the index: + while index < len( lines ): + + line = lines[ index ] + index += 1 + self.pre_index.append( line ) + if line[ : 6 ] == "$INDEX": + break + + m = re.match( r"Units[\s]+mm[\s]*", line ) + if m is not None: + use_mm = True + + # Read the index: + while index < len( lines ): + + line = lines[ index ] + if line[ : 9 ] == "$EndINDEX": + break + index += 1 + self.loaded_modules[ line.strip() ] = [] + + # Read up until the first module: + while index < len( lines ): + + line = lines[ index ] + if line[ : 7 ] == "$MODULE": + break + index += 1 + self.post_index.append( line ) + + # Read modules: + while index < len( lines ): + + line = lines[ index ] + if line[ : 7 ] == "$MODULE": + module_name, module_lines, index = self._read_module( lines, index ) + if module_name is not None: + self.loaded_modules[ module_name ] = module_lines + + elif line[ : 11 ] == "$EndLIBRARY": + break + + else: + raise Exception( + "Expected $EndLIBRARY: [{}]".format( line ) + ) + + return use_mm + + + #------------------------------------------------------------------------ + + def _read_module( self, lines, index ): + + # Read module name: + m = re.match( r'\$MODULE[\s]+([^\s]+)[\s]*', lines[ index ] ) + module_name = m.group( 1 ) + + logging.info( " Reading module {}".format( module_name ) ) + + index += 1 + module_lines = [] + while index < len( lines ): + + line = lines[ index ] + index += 1 + + m = re.match( + r'\$EndMODULE[\s]+' + module_name + r'[\s]*', line + ) + if m is not None: + return module_name, module_lines, index + + module_lines.append( line ) + + raise Exception( + "Could not find end of module '{}'".format( module_name ) + ) + + + #------------------------------------------------------------------------ + + def _write_library_intro( self, cmdline ): + + # Write pre-index: + self.output_file.writelines( self.pre_index ) + + self.loaded_modules[ self._get_module_name( front = True ) ] = None + if self.include_reverse: + self.loaded_modules[ + self._get_module_name( front = False ) + ] = None + + # Write index: + for module_name in sorted( + self.loaded_modules.keys(), + key = str.lower + ): + self.output_file.write( module_name + "\n" ) + + # Write post-index: + self.output_file.writelines( self.post_index ) + + + #------------------------------------------------------------------------ + + def _write_preserved_modules( self, up_to = None ): + + if up_to is not None: + up_to = up_to.lower() + + for module_name in sorted( + self.loaded_modules.keys(), + key = str.lower + ): + if up_to is not None and module_name.lower() >= up_to: + continue + + module_lines = self.loaded_modules[ module_name ] + + if module_lines is not None: + + self.output_file.write( + "$MODULE {}\n".format( module_name ) + ) + self.output_file.writelines( module_lines ) + self.output_file.write( + "$EndMODULE {}\n".format( module_name ) + ) + + self.loaded_modules[ module_name ] = None + + + #------------------------------------------------------------------------ + + def _write_module_footer( self, front ): + + super( Svg2ModExportLegacyUpdater, self )._write_module_footer( + front, + ) + + # Write remaining modules: + if not front: + self._write_preserved_modules() + + + #------------------------------------------------------------------------ + + def _write_module_header( + self, + label_size, + label_pen, + reference_y, + value_y, + front, + ): + self._write_preserved_modules( + up_to = self._get_module_name( front ) + ) + + super( Svg2ModExportLegacyUpdater, self )._write_module_header( + label_size, + label_pen, + reference_y, + value_y, + front, + ) + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +class Svg2ModExportPretty( Svg2ModExport ): + ''' This provides functionality for the + newer kicad "pretty" footprint file formats. + It is a child of Svg2ModExport. + ''' + + layer_map = { + #'inkscape-name' : kicad-name, + '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", + 'Drill.Cu': "Drill.Cu", + 'Drill.Mech': "Drill.Mech", + r'\S+\.Keepout': "Keepout" + } + + keepout_allowed = ['tracks','vias','pads','copperpour','footprints'] + + + #------------------------------------------------------------------------ + + def __init__(self, *args, **kwargs): + super(Svg2ModExportPretty, self).__init__(*args, **kwargs) + self._special_footer = "" + self._extra_indent = 0 + + #------------------------------------------------------------------------ + + def _get_layer_name( self, item_name, name, front ): + + name = self.layer_map[ name ] + + # For pretty format layers can have attributes in the svg name + # This validates all the layers and converts them to a json string + # which is attached to the returned value with `:` + attrs = {} + + # Keepout layer validation and expansion + if name == "Keepout": + attrs["layers"] = [] + layers = re.match(r'^(.*)\.Keepout', item_name[0]).groups()[0] + if len(layers) == 1 and layers not in "BFI*": + raise Exception("Unexpected keepout layer: {} in {}".format(layers, item_name[0])) + if len(layers) == 1: + if layers == '*': + attrs['layers'] = ['F','B','I'] + else: + attrs['layers'] = [layers] + else: + for layer in layers: + if layer not in "FBI": + raise Exception("Unexpected keepout layer: {} in {}".format(layer, item_name[0])) + if layer != '&': + attrs['layers'].append(layer) + + # All attributes with the exception of keepout layers is validated + if len(item_name) == 2 and item_name[1]: + for arg in item_name[1].split(';'): + arg = arg.strip(' ,:') + if name == "Keepout" and re.match(r'^allowed:\w+', arg, re.I): + attrs["allowed"] = [] + for allowed in arg.lower().split(":", 1)[1].split(','): + if allowed in self.keepout_allowed: + attrs["allowed"].append(allowed) + else: + logging.warning("Invalid allowed option in keepout: {} in {}".format(allowed, arg)) + elif re.match(r'^\w+\.Cu', name) and re.match(r'^pad(:(\d+|mask|paste))?', arg, re.I): + if arg.lower() == "pad": + attrs["copper_pad"] = True + else: + ops = arg.split(":", 1)[1] + for opt in ops.split(','): + if re.match(r'^\d+$', opt): + attrs["copper_pad"] = int(opt) + elif opt.lower() == "mask" and name != "Drill.Cu": + attrs["pad_mask"] = True + if not attrs.get("copper_pad"): + attrs["copper_pad"] = True + elif opt.lower() == "paste" and name != "Drill.Cu": + attrs["pad_paste"] = True + if not attrs.get("copper_pad"): + attrs["copper_pad"] = True + else: + logging.warning("Invalid pad option '{}' for layer {}".format(opt, name)) + else: + logging.warning("Unexpected option: {} for {}".format(arg, item_name[0])) + if attrs: + return name+":"+json.dumps(attrs) + return name + + + #------------------------------------------------------------------------ + + def _get_module_name( self, front = None ): + + return self.imported.module_name + + + #------------------------------------------------------------------------ + + def _write_library_intro( self, cmdline ): + + self.output_file.write( """(module {0} (layer F.Cu) (tedit {1:8X}) + (attr virtual) + (descr "{2}") + (tags {3}) +""".format( + self.imported.module_name, #0 + int( round( #1 + os.path.getctime( self.imported.file_name ) if self.imported.file_name else time.time() + ) ), + "Converted using: {}".format( cmdline.replace("\\", "\\\\") ), #2 + "svg2mod", #3 + ) + ) + + + #------------------------------------------------------------------------ + + def _write_module_footer( self, front ): + + self.output_file.write( "\n)" ) + + + #------------------------------------------------------------------------ + + def _write_module_header( + self, label_size, label_pen, + reference_y, value_y, front, + ): + if front: + side = "F" + else: + side = "B" + + self.output_file.write( +""" (fp_text reference {0} (at 0 {1}) (layer {2}.SilkS) hide + (effects (font (size {3} {3}) (thickness {4}))) + ) + (fp_text value {5} (at 0 {6}) (layer {2}.SilkS) hide + (effects (font (size {3} {3}) (thickness {4}))) + )""".format( + + self._get_module_name(), #0 + reference_y, #1 + side, #2 + label_size, #3 + label_pen, #4 + self.imported.module_value, #5 + value_y, #6 + ) + ) + + + #------------------------------------------------------------------------ + + def _write_modules( self ): + + self._write_module( front = True ) + + + #------------------------------------------------------------------------ + + def _write_polygon_filled( self, points, layer, stroke_width = 0): + self._write_polygon_header( points, layer, stroke_width) + + for point in points: + self._write_polygon_point( point ) + + self._write_polygon_footer( layer, stroke_width ) + + + #------------------------------------------------------------------------ + + def _write_polygon( self, points, layer, fill, stroke, stroke_width ): + + if fill: + self._write_polygon_filled( + points, layer, stroke_width + ) + + # Polygons with a fill and stroke are drawn with the filled polygon + # above: + if stroke and not fill: + + self._write_polygon_outline( + points, layer, stroke_width + ) + + + #------------------------------------------------------------------------ + + def _write_polygon_footer( self, layer, stroke_width ): + + if self._special_footer: + self.output_file.write(self._special_footer) + else: + self.output_file.write( + " )\n (layer {})\n (width {})\n )".format( + layer.split(":", 1)[0], stroke_width + ) + ) + self._special_footer = "" + self._extra_indent = 0 + + + #------------------------------------------------------------------------ + + def _write_polygon_header( self, points, layer, stroke_width): + + l_name = layer + options = {} + try: + l_name, options = layer.split(":", 1) + options = json.loads(options) + except ValueError:pass + + create_pad = (self.convert_pads and l_name.find("Cu") == 2) or options.get("copper_pad") + + if stroke_width == 0: + stroke_width = 1e-5 #This is the smallest a pad can be and still be rendered in kicad + + if l_name == "Keepout": + self._extra_indent = 1 + layers = ["*"] + if len(options["layers"]) == 3: + layers = ["*"] + elif "I" not in options["layers"]: + layers = ["&".join(options["layers"])] + else: + options["layers"].remove("I") + layers = options["layers"][:] + ['In{}'.format(i) for i in range(1,31)] + + + self.output_file.write( '''\n (zone (net 0) (net_name "") (layers "{0}.Cu") (hatch edge {1:.6f}) + (connect_pads (clearance 0)) + (min_thickness {2:.6f}) + (keepout ({3}allowed)) + (fill (thermal_gap {1:.6f}) (thermal_bridge_width {1:.6f})) + (polygon + (pts\n'''.format( + '.Cu" "'.join(layers), #0 + stroke_width, #1 + stroke_width/2, #2 + "allowed) (".join( + [i+" "+( + "not_" if not options.get("allowed") or i not in options["allowed"] else "" + ) for i in self.keepout_allowed] + ), #3 + ) + ) + self._special_footer = " )\n )\n )" + elif create_pad: + pad_number = "" if not options.get("copper_pad") or isinstance(options["copper_pad"], bool) else str(options.get("copper_pad")) + layer = l_name + if options.get("pad_mask"): + layer += " {}.Mask".format(l_name.split(".", 1)[0]) + if options.get("pad_paste"): + layer += " {}.Paste".format(l_name.split(".", 1)[0]) + self._extra_indent = 1 + self._special_footer = " )\n (width {}) )\n ))".format( + stroke_width + ) + self.output_file.write( '''\n (pad "{0}" smd custom (at {1} {2}) (size {3:.6f} {3:.6f}) (layers {4}) + (zone_connect 0) + (options (clearance outline) (anchor circle)) + (primitives\n (gr_poly (pts \n'''.format( + pad_number, #0 + points[0].x, #1 + points[0].y, #2 + stroke_width, #3 + layer, #4 + ) + ) + originx = points[0].x + originy = points[0].y + for point in points: + point.x = point.x-originx + point.y = point.y-originy + else: + self.output_file.write( "\n (fp_poly\n (pts \n" ) + + + #------------------------------------------------------------------------ + + def _write_polygon_point( self, point ): + + if self._extra_indent: + self.output_file.write(" "*self._extra_indent) + + self.output_file.write( + " (xy {} {})\n".format( point.x, point.y ) + ) + + + #------------------------------------------------------------------------ + + def _write_polygon_segment( self, p, q, layer, stroke_width ): + + self.output_file.write( + """\n (fp_line + (start {} {}) + (end {} {}) + (layer {}) + (width {}) + )""".format( + p.x, p.y, + q.x, q.y, + layer, + stroke_width, + ) + ) + + + #------------------------------------------------------------------------ + + def _write_thru_hole( self, circle, layer ): + + l_name = layer + options = {} + try: + l_name, options = layer.split(":", 1) + options = json.loads(options) + except ValueError:pass + + plated = l_name == "Drill.Cu" + pad_number = "" + if plated and options.get("copper_pad") and not isinstance(options["copper_pad"], bool): + pad_number = str(options.get("copper_pad")) + + rad = circle.rx * self.scale_factor + drill = rad * 2 + + size = circle.style.get("stroke-width") * self.scale_factor + if size and plated: + drill -= size + size = size + ( rad * 2 ) + elif size: + size = size + ( rad * 2 ) + drill = size + else: + size = rad + + + self.output_file.write( + '\n (pad "{0}" {1}thru_hole circle (at {2} {3}) (size {4} {4}) (drill {5}) (layers *.Mask{6}) {7})'.format( + pad_number, #0 + "" if plated else "np_", #1 + circle.center.x * self.scale_factor, #2 + circle.center.y * self.scale_factor, #3 + size, #4 + drill, #5 + " *.Cu" if plated else "", #6 + "(remove_unused_layers) (keep_end_layers)" if plated else "", #7 + ) + ) + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- diff --git a/svg2mod/importer.py b/svg2mod/importer.py new file mode 100644 index 0000000..db25080 --- /dev/null +++ b/svg2mod/importer.py @@ -0,0 +1,71 @@ +# Copyright (C) 2021 -- svg2mod developers < GitHub.com / svg2mod > + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +''' +Svg2ModImport is responsible for basic parsing +of svg layers to be used with an instance of +Svg2ModExport. +''' + +import logging + +from svg2mod import svg + + +class Svg2ModImport: + ''' An importer class to read in target svg, + parse it, and keep only layers on interest. + ''' + + + def _prune_hidden( self, items = None ): + + if items is None: + + items = self.svg.items + self.svg.items = [] + + for item in items: + + if not isinstance( item, svg.Group ): + continue + + if item.hidden : + logging.warning("Ignoring hidden SVG layer: {}".format( item.name ) ) + elif item.name != "": + self.svg.items.append( item ) + + if item.items: + self._prune_hidden( item.items ) + + def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", ignore_hidden_layers=False ): + + self.file_name = file_name + self.module_name = module_name + self.module_value = module_value + + if file_name: + logging.getLogger("unfiltered").info( "Parsing SVG..." ) + + self.svg = svg.parse( file_name) + logging.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) + if ignore_hidden_layers: + self._prune_hidden() + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 86f7bd6..bb398c9 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -1,5 +1,3 @@ -# SVG parser in Python - # Copyright (C) 2013 -- CJlano < cjlano @ free.fr > # Copyright (C) 2022 -- svg2mod developers < GitHub.com / svg2mod > @@ -21,26 +19,25 @@ to objects that can be simplified into points. ''' -import xml.etree.ElementTree as etree -from typing import List, Tuple -import math -import sys -import os import copy -import re +import inspect import itertools -import operator -import platform import json import logging -import inspect +import math +import operator +import os +import platform +import re +import sys +import xml.etree.ElementTree as etree +from typing import List, Tuple -from fontTools.ttLib import ttFont -from fontTools.pens.svgPathPen import SVGPathPen from fontTools.misc import loggingTools +from fontTools.pens.svgPathPen import SVGPathPen +from fontTools.ttLib import ttFont -from .geometry import Point,Angle,Segment,Bezier,MoveTo,simplify_segment - +from .geometry import Angle, Bezier, MoveTo, Point, Segment, simplify_segment svg_ns = '{http://www.w3.org/2000/svg}' @@ -69,6 +66,10 @@ class Transformable: '''Abstract class for objects that can be geometrically drawn & transformed''' + + # This list is all styles that should have the transformation matrix applied + transformable_styles = ["stroke-width"] + def __init__(self, elt=None): # a 'Transformable' is represented as a list of Transformable items self.items = [] @@ -77,11 +78,33 @@ def __init__(self, elt=None): self.matrix = Matrix() self.scalex = 1 self.scaley = 1 - self.style = "" + self.style = {} self.rotation = 0 self.viewport = Point(800, 600) # default viewport is 800x600 if elt is not None: self.id = elt.get('id', self.id) + + # parse styles and save as dictionary. + if elt.get('style'): + for style in elt.get('style').split(";"): + if style.find(":") == -1: + self.style[name] = None + else: + nv = style.split(":") + name = nv[ 0 ].strip() + value = nv[ 1 ].strip() + if name in self.transformable_styles: + value = list(re.search(r'(\d+\.?\d*)(\D+)?', value).groups()) + self.style[name] = float(value[0]) + # if not value[1]: + # TODO verify that mm is the default value for more than stroke-width + # value[1] = "mm" + if value[1] and value[1] not in unit_convert: + logging.warning("Style '{}' has an unexpected unit: {}".format(style, value[1])) + # self.style[name] /= unit_convert[value[1]] + else: + self.style[name] = value + # Parse transform attribute to update self.matrix self.get_transformations(elt) @@ -157,6 +180,15 @@ def get_transformations(self, elt): tana = math.tan(math.radians(arg[0])) self.matrix *= Matrix([1, tana, 0, 1, 0, 0]) + def transform_styles(self, matrix): + '''Any style in this classes transformable_styles + will be scaled by the provided matrix. + ''' + for style in self.transformable_styles: + if self.style.get(style): + self.style[style] = float(self.style[style]) * ((matrix.xscale()+matrix.yscale())/2) + + def transform(self, matrix=None): '''Apply the provided matrix. Default (None) If no matrix is supplied then recursively apply @@ -166,6 +198,7 @@ def transform(self, matrix=None): matrix = self.matrix else: matrix *= self.matrix + self.transform_styles(matrix) #print( "do transform: {}: {}".format( self.__class__.__name__, matrix ) ) #print( "do transform: {}: {}".format( self, matrix ) ) #traceback.print_stack() @@ -395,12 +428,6 @@ def __mul__(self, other): def __str__(self): return str(self.vect) - def xlength(self, x): - '''x scale of vector''' - return x * self.vect[0] - def ylength(self, y): - '''y scale of vector''' - return y * self.vect[3] def xscale(self): '''Return the rotated x scalar value''' return self.vect[0]/abs(self.vect[0]) * math.sqrt(self.vect[0]**2 + self.vect[2]**2) @@ -433,7 +460,6 @@ class Path(Transformable): def __init__(self, elt=None): Transformable.__init__(self, elt) if elt is not None: - self.style = elt.get('style') self.parse(elt.get('d')) def parse(self, pathstr:str): @@ -627,7 +653,6 @@ def __init__(self, elt=None): self.ylength(elt.get('cy'))) self.rx = self.length(elt.get('rx')) self.ry = self.length(elt.get('ry')) - self.style = elt.get('style') if elt.get('d') is not None: self.arc = True self.path = Path(elt) @@ -657,6 +682,8 @@ def transform(self, matrix=None): matrix = self.matrix else: matrix *= self.matrix + self.transform_styles(matrix) + self.center = matrix * self.center self.rx = matrix.xscale()*self.rx self.ry = matrix.yscale()*self.ry @@ -908,7 +935,6 @@ class Rect(Path): def __init__(self, elt=None): Transformable.__init__(self, elt) if elt is not None: - self.style = elt.get('style') p = Point(self.xlength(elt.get('x')), self.ylength(elt.get('y'))) width = self.xlength(elt.get("width")) @@ -918,6 +944,8 @@ def __init__(self, elt=None): ry = self.xlength(elt.get('ry')) if not rx: rx = ry if ry else 0 if not ry: ry = rx if rx else 0 + if rx > width/2: rx = width/2 + if ry > height/2: ry = width/2 if rx or ry: cmd = f'''M{p.x+rx} {p.y} a{rx} {ry} 0 0 0 {-rx} {ry} v{height-(ry*2)} a{rx} {ry} 0 0 0 {rx} {ry} h{width-(rx*2)} @@ -975,6 +1003,8 @@ def transform(self, matrix=None): matrix = self.matrix else: matrix *= self.matrix + self.transform_styles(matrix) + self.P1 = matrix * self.P1 self.P2 = matrix * self.P2 self.segment = Segment(self.P1, self.P2) @@ -1026,7 +1056,6 @@ def __init__(self, elt=None, parent=None): self.paths = [] if elt is not None: - self.style = elt.get('style') self.parse(elt, parent) if parent is None: self.convert_to_path(auto_transform=False) @@ -1100,14 +1129,9 @@ def parse(self, elt, parent): "font-weight": elt.get('font-weight'), "font-style": elt.get('font-style'), } - if self.style is not None: - for style in self.style.split(";"): - if style.find(":") == -1: continue - nv = style.split(":") - name = nv[ 0 ].strip() - value = nv[ 1 ].strip() - if list(self.font_configs.keys()).count(name) != 0: - self.font_configs[name] = value + for style in self.style: + if style in self.font_configs.keys() and self.style[style]: + self.font_configs[name] = self.style[style] if isinstance(self.font_configs["font-size"], str): self.font_configs["font-size"] = float(self.font_configs["font-size"].strip("px")) @@ -1250,7 +1274,7 @@ def convert_to_path(self, auto_transform=True): pathbuf = "" try: glf = ttf.getGlyphSet()[ttf.getBestCmap()[ord(char)]] except KeyError: - logging.warning(f"Unsuported character in element \"{char}\"") + logging.warning('Unsuported character in element "{}"'.format(char)) #txt = txt.replace(char, "") continue @@ -1298,6 +1322,8 @@ def transform(self, matrix=None): matrix = self.matrix else: matrix *= self.matrix + self.transform_styles(matrix) + self.origin = matrix * self.origin for paths in self.paths: for path in paths: diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index adaf9eb..59cb17e 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -15,162 +15,14 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ''' -This module contains the necessary tools to convert from -the svg objects provided from the svg2mod.svg module to -KiCad file formats. -This currently supports both the pretty format and -the legacy mod format. +Helper classes to combine and calculate the points +from a svg object into a single continuous line ''' -from abc import ABC, abstractmethod -from typing import List, Tuple -import argparse -import datetime -import shlex -import os -import sys -import re -import io -import time import logging +from typing import List, Tuple -import svg2mod.svg as svg -import svg2mod.coloredlogger as coloredlogger - - -#---------------------------------------------------------------------------- -DEFAULT_DPI = 96 # 96 as of Inkscape 0.92 - -def main(): - '''This function handles the scripting package calls. - It is setup to read the arguments from `get_arguments()` - then parse the target svg file and output all converted - objects into a kicad footprint module. - ''' - - args,_ = get_arguments() - - - # Setup root logger to use terminal colored outputs as well as stdout and stderr - coloredlogger.split_logger(logging.root) - - if args.verbose_print: - logging.root.setLevel(logging.INFO) - elif args.debug_print: - logging.root.setLevel(logging.DEBUG) - else: - logging.root.setLevel(logging.WARNING) - - # Add a second logger that will bypass the log level and output anyway - # It is a good practice to send only messages level INFO via this logger - logging.getLogger("unfiltered").setLevel(logging.INFO) - - # This can be used sparingly as follows: - ########## - # logging.getLogger("unfiltered").info("Message Here") - ########## - - if args.list_fonts: - fonts = svg.Text.load_system_fonts() - logging.getLogger("unfiltered").info("Font Name: list of supported styles.") - for font in fonts: - fnt_text = f" {font}:" - for styles in fonts[font]: - fnt_text += f" {styles}," - fnt_text = fnt_text.strip(",") - logging.getLogger("unfiltered").info(fnt_text) - sys.exit(0) - if args.default_font: - svg.Text.default_font = args.default_font - - pretty = args.format == 'pretty' - use_mm = args.units == 'mm' - - if pretty: - - if not use_mm: - logging.critical("Error: decimal units only allowed with legacy output type") - sys.exit( -1 ) - - #if args.include_reverse: - #print( - #"Warning: reverse footprint not supported or required for" + - #" pretty output format" - #) - - # Import the SVG: - imported = Svg2ModImport( - args.input_file_name, - args.module_name, - args.module_value, - args.ignore_hidden_layers, - ) - - # Pick an output file name if none was provided: - if args.output_file_name is None: - - args.output_file_name = os.path.splitext( - os.path.basename( args.input_file_name ) - )[ 0 ] - - # Append the correct file name extension if needed: - if pretty: - extension = ".kicad_mod" - else: - extension = ".mod" - if args.output_file_name[ - len( extension ) : ] != extension: - args.output_file_name += extension - - # Create an exporter: - if pretty: - exported = Svg2ModExportPretty( - imported, - args.output_file_name, - args.center, - args.scale_factor, - args.precision, - dpi = args.dpi, - pads = args.convert_to_pads, - ) - - else: - - # If the module file exists, try to read it: - exported = None - if os.path.isfile( args.output_file_name ): - - try: - exported = Svg2ModExportLegacyUpdater( - imported, - args.output_file_name, - args.center, - args.scale_factor, - args.precision, - args.dpi, - ) - - except Exception as e: - raise e - #print( e.message ) - #exported = None - - # Write the module file: - if exported is None: - exported = Svg2ModExportLegacy( - imported, - args.output_file_name, - args.center, - args.scale_factor, - args.precision, - use_mm = use_mm, - dpi = args.dpi, - ) - - args = [os.path.basename(sys.argv[0])] + sys.argv[1:] - cmdline = ' '.join(shlex.quote(x) for x in args) - - # Export the footprint: - exported.write(cmdline) +from svg2mod import svg #---------------------------------------------------------------------------- @@ -352,6 +204,9 @@ def __init__( self, points:List): if len( points ) < 3: logging.warning("Warning: Path segment has only {} points (not a polygon?)".format(len( points ))) + self.bbox = None + self.calc_bbox() + #------------------------------------------------------------------------ @@ -371,10 +226,10 @@ def _find_insertion_point( self, hole: 'PolygonSegment', holes: list, other_inse highest_point = max(hole.points, key=lambda v: v.y) vertical_line = LineSegment(highest_point, svg.Point(highest_point.x, self.bbox[1].y+1)) - intersections = {} - for h in holes: - if h == hole:continue - intersections[h] = h.intersects(vertical_line, False, count_intersections=True, get_points=True) + intersections = {self: self.intersects(vertical_line, False, count_intersections=True, get_points=True)} + for _,h,__ in other_insertions: + if h.bbox[0].x < highest_point.x and h.bbox[1].x > highest_point.x: + intersections[h] = h.intersects(vertical_line, False, count_intersections=True, get_points=True) best = [self, intersections[self][0]] best.append(LineSegment.vertical_intersection(best[1][0], best[1][1], highest_point.x)) @@ -495,7 +350,7 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int if get_points: intersect_segs.append((hole_segment.p, hole_segment.q)) else: - # If line_segment passes through a point this prevents a second false positive + # If line_segment passes thru a point this prevents a second false positive intersections += 0 if line_segment.on_line(hole_segment.q) else 1 elif get_points: return hole_segment.p, hole_segment.q @@ -512,7 +367,7 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int #------------------------------------------------------------------------ def process( self, transformer, flip, fill ): - ''' Apply all transformations and rounding, then remove duplicate + ''' Apply all transformations, then remove duplicate consecutive points along the path. ''' @@ -590,1280 +445,3 @@ def are_distinct(self, polygon): #---------------------------------------------------------------------------- -class Svg2ModImport: - ''' An importer class to read in target svg, - parse it, and keep only layers on interest. - ''' - - - def _prune_hidden( self, items = None ): - - if items is None: - - items = self.svg.items - self.svg.items = [] - - for item in items: - - if not isinstance( item, svg.Group ): - continue - - if item.hidden : - logging.warning("Ignoring hidden SVG layer: {}".format( item.name ) ) - elif item.name != "": - self.svg.items.append( item ) - - if item.items: - self._prune_hidden( item.items ) - - def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", ignore_hidden_layers=False ): - - self.file_name = file_name - self.module_name = module_name - self.module_value = module_value - - if file_name: - logging.getLogger("unfiltered").info( "Parsing SVG..." ) - - self.svg = svg.parse( file_name) - logging.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) - if ignore_hidden_layers: - self._prune_hidden() - - - #------------------------------------------------------------------------ - -#---------------------------------------------------------------------------- - -class Svg2ModExport(ABC): - ''' An abstract class to provide functionality - to write to kicad module file. - The abstract methods are the file type specific - example: pretty, legacy - ''' - - @property - @abstractmethod - def layer_map(self ): - ''' This should be overwritten by a dictionary object of layer maps ''' - pass - - @abstractmethod - def _get_layer_name( self, name, front ):pass - - @abstractmethod - def _write_library_intro( self, cmdline ): pass - - @abstractmethod - def _get_module_name( self, front = None ): pass - - @abstractmethod - def _write_module_header( self, label_size, label_pen, reference_y, value_y, front,): pass - - @abstractmethod - def _write_modules( self ): pass - - @abstractmethod - def _write_module_footer( self, front ):pass - - @abstractmethod - def _write_polygon_header( self, points, layer ):pass - - @abstractmethod - def _write_polygon( self, points, layer, fill, stroke, stroke_width ):pass - - @abstractmethod - def _write_polygon_footer( self, layer, stroke_width ):pass - - @abstractmethod - def _write_polygon_point( self, point ):pass - - @abstractmethod - def _write_polygon_segment( self, p, q, layer, stroke_width ):pass - - #------------------------------------------------------------------------ - - @staticmethod - def _convert_decimal_to_mm( decimal ): - return float( decimal ) * 0.00254 - - - #------------------------------------------------------------------------ - - @staticmethod - def _convert_mm_to_decimal( mm ): - return int( round( mm * 393.700787 ) ) - - - #------------------------------------------------------------------------ - - def _get_fill_stroke( self, item ): - - fill = True - stroke = True - stroke_width = 0.0 - - if item.style is not None and item.style != "": - - for prprty in filter(None, item.style.split( ";" )): - - nv = prprty.split( ":" ) - name = nv[ 0 ].strip() - value = nv[ 1 ].strip() - - if name == "fill" and value == "none": - fill = False - - elif name == "fill-opacity": - if float(value) == 0: - fill = False - - elif name == "stroke" and value == "none": - stroke = False - - elif name == "stroke-width": - stroke_width = float( "".join(i for i in value if not i.isalpha()) ) - - # units per pixel converted to output units - # TODO: Include all transformations instead of just - # the top-level viewport_scale - scale = self.imported.svg.viewport_scale / self.scale_factor - - # remove unnecessary precision to reduce floating point errors - stroke_width = round(stroke_width/scale, 6) - - elif name == "stroke-opacity": - if float(value) == 0: - stroke = False - - if not stroke: - stroke_width = 0.0 - elif stroke_width is None: - # Give a default stroke width? - stroke_width = self._convert_decimal_to_mm( 1 ) if self.use_mm else 1 - - return fill, stroke, stroke_width - - - #------------------------------------------------------------------------ - - def __init__( - self, - svg2mod_import = Svg2ModImport(), - file_name = None, - center = False, - scale_factor = 1.0, - precision = 20.0, - use_mm = True, - dpi = DEFAULT_DPI, - pads = False, - ): - if use_mm: - # 25.4 mm/in; - scale_factor *= 25.4 / float(dpi) - use_mm = True - else: - # PCBNew uses decimal (10K DPI); - scale_factor *= 10000.0 / float(dpi) - - 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 - self.dpi = dpi - self.convert_pads = pads - - #------------------------------------------------------------------------ - - def add_svg_element(self, elem : svg.Transformable, layer="F.SilkS"): - ''' This can be used to add a svg element - to a specific layer. - If the importer doesn't have a svg element - it will also create an empty Svg object. - ''' - grp = svg.Group() - grp.name = layer - grp.items.append(elem) - try: - self.imported.svg.items.append(grp) - except AttributeError: - self.imported.svg = svg.Svg() - self.imported.svg.items.append(grp) - - #------------------------------------------------------------------------ - - - def _calculate_translation( self ): - - min_point, max_point = self.imported.svg.bbox() - - 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, - ) - - else: - self.translation = svg.Point( - 0.0, - 0.0, - ) - - #------------------------------------------------------------------------ - - def _prune( self, items = None ): - '''Find and keep only the layers of interest.''' - - if items is None: - - self.layers = {} - for name in self.layer_map.keys(): - self.layers[ name ] = None - - items = self.imported.svg.items - self.imported.svg.items = [] - - for item in items: - - if not isinstance( item, svg.Group ): - continue - - for name in self.layers.keys(): - #if re.search( name, item.name, re.I ): - if name == item.name and item.name != "": - logging.getLogger("unfiltered").info( "Found SVG layer: {}".format( item.name ) ) - - self.imported.svg.items.append( item ) - self.layers[ name ] = item - break - else: - self._prune( item.items ) - - - #------------------------------------------------------------------------ - - def _write_items( self, items, layer, flip = False ): - - for item in items: - - if isinstance( item, svg.Group ): - self._write_items( item.items, layer, flip ) - continue - - if isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text)): - - segments = [ - PolygonSegment( segment ) - for segment in item.segments( - precision = self.precision - ) - ] - - fill, stroke, stroke_width = self._get_fill_stroke( item ) - fill = (False if layer == "Edge.Cuts" else fill) - - for segment in segments: - segment.process( self, flip, fill ) - - if len( segments ) > 1: - for poly in segments: - poly.calc_bbox() - segments.sort(key=lambda v: svg.Segment(v.bbox[0], v.bbox[1]).length(), reverse=True) - - while len(segments) > 0: - inlinable = [segments[0]] - for seg in segments[1:]: - if not inlinable[0].are_distinct(seg): - append = True - if len(inlinable) > 1: - for hole in inlinable[1:]: - if not hole.are_distinct(seg): - append = False - break - if append: inlinable.append(seg) - for poly in inlinable: - segments.pop(segments.index(poly)) - if len(inlinable) > 1: - points = inlinable[ 0 ].inline( inlinable[ 1 : ] ) - elif len(inlinable) > 0: - points = inlinable[ 0 ].points - - logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) - - self._write_polygon( - points, layer, fill, stroke, stroke_width - ) - continue - - if len( segments ) > 0: - points = segments[ 0 ].points - - if len ( segments ) == 1: - - logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) - - self._write_polygon( - points, layer, fill, stroke, stroke_width - ) - else: - logging.info( " Skipping {} with 0 points".format(item.__class__.__name__)) - - else: - logging.warning( "Unsupported SVG element: {}".format(item.__class__.__name__)) - - - #------------------------------------------------------------------------ - - def _write_module( self, front ): - - module_name = self._get_module_name( front ) - - min_point, max_point = self.imported.svg.bbox() - min_point = self.transform_point( min_point, flip = False ) - max_point = self.transform_point( max_point, flip = False ) - - label_offset = 1200 - label_size = 600 - label_pen = 120 - - if self.use_mm: - label_size = self._convert_decimal_to_mm( label_size ) - label_pen = self._convert_decimal_to_mm( label_pen ) - reference_y = min_point.y - self._convert_decimal_to_mm( label_offset ) - value_y = max_point.y + self._convert_decimal_to_mm( label_offset ) - else: - reference_y = min_point.y - label_offset - value_y = max_point.y + label_offset - - self._write_module_header( - label_size, label_pen, - reference_y, value_y, - front, - ) - - for name, group in self.layers.items(): - - if group is None: continue - - layer = self._get_layer_name( name, front ) - - #print( " Writing layer: {}".format( name ) ) - self._write_items( group.items, layer, not front ) - - self._write_module_footer( front ) - - - #------------------------------------------------------------------------ - - def _write_polygon_filled( self, points, layer, stroke_width = 0.0 ): - - self._write_polygon_header( points, layer ) - - for point in points: - self._write_polygon_point( point ) - - self._write_polygon_footer( layer, stroke_width ) - - - #------------------------------------------------------------------------ - - def _write_polygon_outline( self, points, layer, stroke_width ): - - prior_point = None - for point in points: - - if prior_point is not None: - - self._write_polygon_segment( - prior_point, point, layer, stroke_width - ) - - prior_point = point - - - #------------------------------------------------------------------------ - - def transform_point( self, point, flip = False ): - ''' Transform provided point by this - classes scale factor. - ''' - - transformed_point = svg.Point( - ( point.x + self.translation.x ) * self.scale_factor, - ( point.y + self.translation.y ) * self.scale_factor, - ) - - if flip: - transformed_point.x *= -1 - - if self.use_mm: - transformed_point.x = round( transformed_point.x, 12 ) - transformed_point.y = round( transformed_point.y, 12 ) - else: - transformed_point.x = int( round( transformed_point.x ) ) - transformed_point.y = int( round( transformed_point.y ) ) - - return transformed_point - - - #------------------------------------------------------------------------ - - def write( self, cmdline="scripting" ): - '''Write the kicad footprint file. - The value from the command line argument - is set in a comment in the header of the file. - - If self.file_name is not null then this will - overwrite the target file with the data provided. - However if it is null then all data is written - to the string IO class (for same API as writing) - then dumped into self.raw_file_data before the - writer is closed. - ''' - - self._prune() - - # Must come after pruning: - self._calculate_translation() - - if self.file_name: - logging.getLogger("unfiltered").info( "Writing module file: {}".format( self.file_name ) ) - self.output_file = open( self.file_name, 'w' ) - else: - self.output_file = io.StringIO() - - self._write_library_intro(cmdline) - - self._write_modules() - - if self.file_name is None: - self.raw_file_data = self.output_file.getvalue() - - self.output_file.close() - self.output_file = None - - - #------------------------------------------------------------------------ - -#---------------------------------------------------------------------------- - -class Svg2ModExportLegacy( Svg2ModExport ): - ''' A child of Svg2ModExport that implements - specific functionality for kicad legacy file types - ''' - - layer_map = { - #'inkscape-name' : [ kicad-front, kicad-back ], - '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 ], - 'Eco2.User' : [ 27, 27 ], - 'Edge.Cuts' : [ 28, 28 ], - } - - - #------------------------------------------------------------------------ - - def __init__( - self, - svg2mod_import, - file_name, - center, - scale_factor = 1.0, - precision = 20.0, - use_mm = True, - dpi = DEFAULT_DPI, - ): - super( Svg2ModExportLegacy, self ).__init__( - svg2mod_import, - file_name, - center, - scale_factor, - precision, - use_mm, - dpi, - pads = False, - ) - - self.include_reverse = True - - - #------------------------------------------------------------------------ - - def _get_layer_name( self, name, front ): - - layer_info = self.layer_map[ name ] - layer = layer_info[ 0 ] - if not front and layer_info[ 1 ] is not None: - layer = layer_info[ 1 ] - - return layer - - - #------------------------------------------------------------------------ - - def _get_module_name( self, front = None ): - - if self.include_reverse and not front: - return self.imported.module_name + "-rev" - - return self.imported.module_name - - - #------------------------------------------------------------------------ - - def _write_library_intro( self, cmdline ): - - modules_list = self._get_module_name( front = True ) - if self.include_reverse: - modules_list += ( - "\n" + - self._get_module_name( front = False ) - ) - - units = "" - if self.use_mm: - units = "\nUnits mm" - - self.output_file.write( """PCBNEW-LibModule-V1 {0}{1} -$INDEX -{2} -$EndINDEX -# -# Converted using: {3} -# -""".format( - datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ), - units, - modules_list, - cmdline.replace("\\","\\\\") - ) - ) - - - #------------------------------------------------------------------------ - - def _write_module_header( - self, label_size, label_pen, - reference_y, value_y, front, - ): - - self.output_file.write( """$MODULE {0} -Po 0 0 0 {6} 00000000 00000000 ~~ -Li {0} -T0 0 {1} {2} {2} 0 {3} N I 21 "{0}" -T1 0 {5} {2} {2} 0 {3} N I 21 "{4}" -""".format( - self._get_module_name( front ), - reference_y, - label_size, - label_pen, - self.imported.module_value, - value_y, - 15, # Seems necessary - ) - ) - - - #------------------------------------------------------------------------ - - def _write_module_footer( self, front ): - - self.output_file.write( - "$EndMODULE {0}\n".format( self._get_module_name( front ) ) - ) - - - #------------------------------------------------------------------------ - - def _write_modules( self ): - - self._write_module( front = True ) - - if self.include_reverse: - self._write_module( front = False ) - - self.output_file.write( "$EndLIBRARY" ) - - - #------------------------------------------------------------------------ - - def _write_polygon( self, points, layer, fill, stroke, stroke_width ): - - if fill: - self._write_polygon_filled( - points, layer - ) - - if stroke: - - self._write_polygon_outline( - points, layer, stroke_width - ) - - - #------------------------------------------------------------------------ - - def _write_polygon_footer( self, layer, stroke_width ): - - pass - - - #------------------------------------------------------------------------ - - def _write_polygon_header( self, points, layer ): - - pen = 1 - if self.use_mm: - pen = self._convert_decimal_to_mm( pen ) - - self.output_file.write( "DP 0 0 0 0 {} {} {}\n".format( - len( points ), - pen, - layer - ) ) - - - #------------------------------------------------------------------------ - - def _write_polygon_point( self, point ): - - self.output_file.write( - "Dl {} {}\n".format( point.x, point.y ) - ) - - - #------------------------------------------------------------------------ - - def _write_polygon_segment( self, p, q, layer, stroke_width ): - - self.output_file.write( "DS {} {} {} {} {} {}\n".format( - p.x, p.y, - q.x, q.y, - stroke_width, - layer - ) ) - - - #------------------------------------------------------------------------ - -#---------------------------------------------------------------------------- - -class Svg2ModExportLegacyUpdater( Svg2ModExportLegacy ): - ''' A Svg2Mod exporter class that reads some settings - from an already existing module and will append its - changes to the file. - ''' - - #------------------------------------------------------------------------ - - def __init__( - self, - svg2mod_import, - file_name, - center, - scale_factor = 1.0, - precision = 20.0, - dpi = DEFAULT_DPI, - include_reverse = True, - ): - self.file_name = file_name - use_mm = self._parse_output_file() - - super( Svg2ModExportLegacyUpdater, self ).__init__( - svg2mod_import, - file_name, - center, - scale_factor, - precision, - use_mm, - dpi, - ) - - - #------------------------------------------------------------------------ - - def _parse_output_file( self ): - - logging.info( "Parsing module file: {}".format( self.file_name ) ) - module_file = open( self.file_name, 'r' ) - lines = module_file.readlines() - module_file.close() - - self.loaded_modules = {} - self.post_index = [] - self.pre_index = [] - use_mm = False - - index = 0 - - # Find the start of the index: - while index < len( lines ): - - line = lines[ index ] - index += 1 - self.pre_index.append( line ) - if line[ : 6 ] == "$INDEX": - break - - m = re.match( r"Units[\s]+mm[\s]*", line ) - if m is not None: - use_mm = True - - # Read the index: - while index < len( lines ): - - line = lines[ index ] - if line[ : 9 ] == "$EndINDEX": - break - index += 1 - self.loaded_modules[ line.strip() ] = [] - - # Read up until the first module: - while index < len( lines ): - - line = lines[ index ] - if line[ : 7 ] == "$MODULE": - break - index += 1 - self.post_index.append( line ) - - # Read modules: - while index < len( lines ): - - line = lines[ index ] - if line[ : 7 ] == "$MODULE": - module_name, module_lines, index = self._read_module( lines, index ) - if module_name is not None: - self.loaded_modules[ module_name ] = module_lines - - elif line[ : 11 ] == "$EndLIBRARY": - break - - else: - raise Exception( - "Expected $EndLIBRARY: [{}]".format( line ) - ) - - return use_mm - - - #------------------------------------------------------------------------ - - def _read_module( self, lines, index ): - - # Read module name: - m = re.match( r'\$MODULE[\s]+([^\s]+)[\s]*', lines[ index ] ) - module_name = m.group( 1 ) - - logging.info( " Reading module {}".format( module_name ) ) - - index += 1 - module_lines = [] - while index < len( lines ): - - line = lines[ index ] - index += 1 - - m = re.match( - r'\$EndMODULE[\s]+' + module_name + r'[\s]*', line - ) - if m is not None: - return module_name, module_lines, index - - module_lines.append( line ) - - raise Exception( - "Could not find end of module '{}'".format( module_name ) - ) - - - #------------------------------------------------------------------------ - - def _write_library_intro( self, cmdline ): - - # Write pre-index: - self.output_file.writelines( self.pre_index ) - - self.loaded_modules[ self._get_module_name( front = True ) ] = None - if self.include_reverse: - self.loaded_modules[ - self._get_module_name( front = False ) - ] = None - - # Write index: - for module_name in sorted( - self.loaded_modules.keys(), - key = str.lower - ): - self.output_file.write( module_name + "\n" ) - - # Write post-index: - self.output_file.writelines( self.post_index ) - - - #------------------------------------------------------------------------ - - def _write_preserved_modules( self, up_to = None ): - - if up_to is not None: - up_to = up_to.lower() - - for module_name in sorted( - self.loaded_modules.keys(), - key = str.lower - ): - if up_to is not None and module_name.lower() >= up_to: - continue - - module_lines = self.loaded_modules[ module_name ] - - if module_lines is not None: - - self.output_file.write( - "$MODULE {}\n".format( module_name ) - ) - self.output_file.writelines( module_lines ) - self.output_file.write( - "$EndMODULE {}\n".format( module_name ) - ) - - self.loaded_modules[ module_name ] = None - - - #------------------------------------------------------------------------ - - def _write_module_footer( self, front ): - - super( Svg2ModExportLegacyUpdater, self )._write_module_footer( - front, - ) - - # Write remaining modules: - if not front: - self._write_preserved_modules() - - - #------------------------------------------------------------------------ - - def _write_module_header( - self, - label_size, - label_pen, - reference_y, - value_y, - front, - ): - self._write_preserved_modules( - up_to = self._get_module_name( front ) - ) - - super( Svg2ModExportLegacyUpdater, self )._write_module_header( - label_size, - label_pen, - reference_y, - value_y, - front, - ) - - - #------------------------------------------------------------------------ - -#---------------------------------------------------------------------------- - -class Svg2ModExportPretty( Svg2ModExport ): - ''' This provides functionality for the - newer kicad "pretty" footprint file formats. - It is a child of Svg2ModExport. - ''' - - layer_map = { - #'inkscape-name' : kicad-name, - '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" - } - - - #------------------------------------------------------------------------ - - def _get_layer_name( self, name, front ): - - return self.layer_map[ name ] - - - #------------------------------------------------------------------------ - - def _get_module_name( self, front = None ): - - return self.imported.module_name - - - #------------------------------------------------------------------------ - - def _write_library_intro( self, cmdline ): - - self.output_file.write( """(module {0} (layer F.Cu) (tedit {1:8X}) - (attr virtual) - (descr "{2}") - (tags {3}) -""".format( - self.imported.module_name, #0 - int( round( #1 - os.path.getctime( self.imported.file_name ) if self.imported.file_name else time.time() - ) ), - "Converted using: {}".format( cmdline.replace("\\", "\\\\") ), #2 - "svg2mod", #3 - ) - ) - - - #------------------------------------------------------------------------ - - def _write_module_footer( self, front ): - - self.output_file.write( "\n)" ) - - - #------------------------------------------------------------------------ - - def _write_module_header( - self, label_size, label_pen, - reference_y, value_y, front, - ): - if front: - side = "F" - else: - side = "B" - - self.output_file.write( -""" (fp_text reference {0} (at 0 {1}) (layer {2}.SilkS) hide - (effects (font (size {3} {3}) (thickness {4}))) - ) - (fp_text value {5} (at 0 {6}) (layer {2}.SilkS) hide - (effects (font (size {3} {3}) (thickness {4}))) - )""".format( - - self._get_module_name(), #0 - reference_y, #1 - side, #2 - label_size, #3 - label_pen, #4 - self.imported.module_value, #5 - value_y, #6 - ) - ) - - - #------------------------------------------------------------------------ - - def _write_modules( self ): - - self._write_module( front = True ) - - - #------------------------------------------------------------------------ - - def _write_polygon_filled( self, points, layer, stroke_width = 0): - self._write_polygon_header( points, layer, stroke_width) - - for point in points: - self._write_polygon_point( point ) - - self._write_polygon_footer( layer, stroke_width ) - - - #------------------------------------------------------------------------ - - def _write_polygon( self, points, layer, fill, stroke, stroke_width ): - - if fill: - self._write_polygon_filled( - points, layer, stroke_width - ) - - # Polygons with a fill and stroke are drawn with the filled polygon - # above: - if stroke and not fill: - - self._write_polygon_outline( - points, layer, stroke_width - ) - - - #------------------------------------------------------------------------ - - def _write_polygon_footer( self, layer, stroke_width ): - - if self._create_pad: - self.output_file.write( - " )\n (width {}) )\n ))".format( - stroke_width - ) - ) - else: - self.output_file.write( - " )\n (layer {})\n (width {})\n )".format( - layer, stroke_width - ) - ) - - - #------------------------------------------------------------------------ - - def _write_polygon_header( self, points, layer, stroke_width): - self._create_pad = self.convert_pads and layer.find("Cu") == 2 - if stroke_width == 0: - stroke_width = 1e-5 #This is the smallest a pad can be and still be rendered in kicad - - if self._create_pad: - self.output_file.write( '''\n (pad 1 smd custom (at {0} {1}) (size {2:.6f} {2:.6f}) (layers {3}) - (zone_connect 0) - (options (clearance outline) (anchor circle)) - (primitives\n (gr_poly (pts \n'''.format( - points[0].x, #0 - points[0].y, #1 - stroke_width, #2 - layer, #3 - ) - ) - originx = points[0].x - originy = points[0].y - for point in points: - point.x = point.x-originx - point.y = point.y-originy - else: - self.output_file.write( "\n (fp_poly\n (pts \n" ) - - - #------------------------------------------------------------------------ - - def _write_polygon_point( self, point ): - - if self._create_pad: - self.output_file.write(" ") - - self.output_file.write( - " (xy {} {})\n".format( point.x, point.y ) - ) - - - #------------------------------------------------------------------------ - - def _write_polygon_segment( self, p, q, layer, stroke_width ): - - self.output_file.write( - """\n (fp_line - (start {} {}) - (end {} {}) - (layer {}) - (width {}) - )""".format( - p.x, p.y, - q.x, q.y, - layer, - stroke_width, - ) - ) - - - #------------------------------------------------------------------------ - -#---------------------------------------------------------------------------- - -def get_arguments(): - ''' Return an instance of pythons argument parser - with all the command line functionalities arguments - ''' - - parser = argparse.ArgumentParser( - description = ( - 'Convert Inkscape SVG drawings to KiCad footprint modules.' - ) - ) - - mux = parser.add_mutually_exclusive_group(required=True) - - mux.add_argument( - '-i', '--input-file', - type = str, - dest = 'input_file_name', - metavar = 'FILENAME', - help = "Name of the SVG file", - ) - - parser.add_argument( - '-o', '--output-file', - type = str, - dest = 'output_file_name', - metavar = 'FILENAME', - help = "Name of the module file", - ) - - parser.add_argument( - '-c', '--center', - dest = 'center', - action = 'store_const', - const = True, - help = "Center the module to the center of the bounding box", - default = False, - ) - - parser.add_argument( - '-P', '--convert-pads', - dest = 'convert_to_pads', - action = 'store_const', - const = True, - help = "Convert any artwork on Cu layers to pads", - default = False, - ) - - parser.add_argument( - '-v', '--verbose', - dest = 'verbose_print', - action = 'store_const', - const = True, - help = "Print more verbose messages", - default = False, - ) - - parser.add_argument( - '--debug', - dest = 'debug_print', - action = 'store_const', - const = True, - help = "Print debug level messages", - default = False, - ) - - parser.add_argument( - '-x', '--exclude-hidden', - dest = 'ignore_hidden_layers', - action = 'store_const', - const = True, - help = "Do not export hidden layers", - default = False, - ) - - parser.add_argument( - '-d', '--dpi', - type = int, - dest = 'dpi', - metavar = 'DPI', - help = "DPI of the SVG file (int)", - default = DEFAULT_DPI, - ) - - parser.add_argument( - '-f', '--factor', - type = float, - dest = 'scale_factor', - metavar = 'FACTOR', - help = "Scale paths by this factor", - default = 1.0, - ) - - parser.add_argument( - '-p', '--precision', - type = float, - dest = 'precision', - metavar = 'PRECISION', - help = "Smoothness for approximating curves with line segments. Input is the approximate length for each line segment in SVG pixels (float)", - default = 10.0, - ) - parser.add_argument( - '--format', - type = str, - dest = 'format', - metavar = 'FORMAT', - choices = [ 'legacy', 'pretty' ], - help = "Output module file format (legacy|pretty)", - default = 'pretty', - ) - - parser.add_argument( - '--name', '--module-name', - type = str, - dest = 'module_name', - metavar = 'NAME', - help = "Base name of the module", - default = "svg2mod", - ) - - parser.add_argument( - '--units', - type = str, - dest = 'units', - metavar = 'UNITS', - choices = [ 'decimal', 'mm' ], - help = "Output units, if output format is legacy (decimal|mm)", - default = 'mm', - ) - - parser.add_argument( - '--value', '--module-value', - type = str, - dest = 'module_value', - metavar = 'VALUE', - help = "Value of the module", - default = "G***", - ) - - parser.add_argument( - '-F', '--default-font', - type = str, - dest = 'default_font', - help = "Default font to use if the target font in a text element cannot be found", - ) - - mux.add_argument( - '-l', '--list-fonts', - dest = 'list_fonts', - const = True, - default = False, - action = "store_const", - help = "List all fonts that can be found in common locations", - ) - - return parser.parse_args(), parser - - #------------------------------------------------------------------------ - -#---------------------------------------------------------------------------- -if __name__ == "__main__": - main() - - -#---------------------------------------------------------------------------- From 9427754ed5c30f3cbe1aa6dba36b1f5f5d1a79c3 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 28 Jan 2022 13:34:15 -0700 Subject: [PATCH 117/151] Add readme clarification --- README.md | 54 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 45c6bd8..841ba8c 100644 --- a/README.md +++ b/README.md @@ -95,30 +95,30 @@ svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain This supports the layers listed below. They are the same in inkscape and kicad: -| KiCad layer(s) | KiCad legacy | KiCad pretty | -|:----------------------:|:------------:|:------------:| -| F.Cu [^1] | Yes | Yes | -| B.Cu [^1] | Yes | Yes | -| F.Adhes | Yes | Yes | -| B.Adhes | Yes | Yes | -| F.Paste | Yes | Yes | -| B.Paste | Yes | Yes | -| F.SilkS | Yes | Yes | -| B.SilkS | Yes | Yes | -| F.Mask | Yes | Yes | -| B.Mask | Yes | Yes | -| Dwgs.User | Yes | Yes | -| Cmts.User | Yes | Yes | -| Eco1.User | Yes | Yes | -| Eco2.User | Yes | Yes | -| Edge.Cuts | Yes | Yes | -| F.Fab | -- | Yes | -| B.Fab | -- | Yes | -| F.CrtYd | -- | Yes | -| B.CrtYd | -- | Yes | -| Drill.Cu [^1] | -- | Yes | -| Drill.Mech [^1] | -- | Yes | -| *.Keepout [^1][^2][^3] | -- | Yes | +| KiCad layer(s) | KiCad legacy | KiCad pretty | +|:------------------------:|:------------:|:------------:| +| F.Cu [^1] | Yes | Yes | +| B.Cu [^1] | Yes | Yes | +| F.Adhes | Yes | Yes | +| B.Adhes | Yes | Yes | +| F.Paste | Yes | Yes | +| B.Paste | Yes | Yes | +| F.SilkS | Yes | Yes | +| B.SilkS | Yes | Yes | +| F.Mask | Yes | Yes | +| B.Mask | Yes | Yes | +| Dwgs.User | Yes | Yes | +| Cmts.User | Yes | Yes | +| Eco1.User | Yes | Yes | +| Eco2.User | Yes | Yes | +| Edge.Cuts | Yes | Yes | +| F.Fab | -- | Yes | +| B.Fab | -- | Yes | +| F.CrtYd | -- | Yes | +| B.CrtYd | -- | Yes | +| Drill.Cu [^1] [^2] | -- | Yes | +| Drill.Mech [^1] [^2] | -- | Yes | +| *.Keepout [^1] [^3] [^4] | -- | Yes | Note: If you have a layer `F.Cu`, all of its sub-layers will be treated as `F.Cu` regardless of their names. @@ -162,8 +162,10 @@ Supported Arguments: [^1]: These layers can have arguments when svg2mod is in pretty mode -[^2]: Only works in Kicad versions >= v6. +[^2]: Drills can only be svg circle objects. The stroke width in `Drill.Cu` is the pad size and the fill is the drill size. -[^3]: The * can be { *, F, B, I } or any combination like FB or BI. These options are for Front, Back, and Internal. +[^3]: Only works in Kicad versions >= v6. + +[^4]: The * can be { *, F, B, I } or any combination like FB or BI. These options are for Front, Back, and Internal. From 97468c7c92281dbb386faefb01ebf8af5281ccd7 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 28 Jan 2022 16:10:23 -0700 Subject: [PATCH 118/151] Fix some small bugs and update the example --- examples/svg2mod.svg | 122 ++++++++++++++++++++++++++++++------------- svg2mod/exporter.py | 26 ++++++--- svg2mod/svg/svg.py | 2 +- svg2mod/svg2mod.py | 10 ++-- 4 files changed, 112 insertions(+), 48 deletions(-) diff --git a/examples/svg2mod.svg b/examples/svg2mod.svg index 7c4fb8f..4ab9c93 100644 --- a/examples/svg2mod.svg +++ b/examples/svg2mod.svg @@ -8,7 +8,7 @@ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" sodipodi:docname="svg2mod.svg" - inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" + inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" id="svg8" version="1.1" viewBox="0 0 193.64404 58.867786" @@ -23,11 +23,11 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="3.2720174" - inkscape:cx="350.0961" - inkscape:cy="162.60072" + inkscape:zoom="1.1568328" + inkscape:cx="520.22919" + inkscape:cy="-58.638249" inkscape:document-units="mm" - inkscape:current-layer="layer3" + inkscape:current-layer="layer9" inkscape:document-rotation="0" showgrid="false" fit-margin-top="0" @@ -35,9 +35,9 @@ fit-margin-right="0" fit-margin-bottom="0" inkscape:window-width="1920" - inkscape:window-height="1017" - inkscape:window-x="-8" - inkscape:window-y="-8" + inkscape:window-height="1048" + inkscape:window-x="0" + inkscape:window-y="0" inkscape:window-maximized="1" /> @@ -57,10 +57,53 @@ inkscape:label="Edge.Cuts" style="display:inline" transform="translate(0.28582191,-0.14291026)"> - + + + + + + - + sodipodi:nodetypes="cccccccsscssssssssccccsssscccccscccccccccccccccccccssssssscssscccccccccccsccccc" + inkscape:connector-curvature="0" /> + sodipodi:nodetypes="cscscscscscsscscscscss" + inkscape:connector-curvature="0" /> + + + - + sodipodi:nodetypes="cscscssssscssssscscssssscscssscscssscscssssscssssscsssccsscssscssssscscssscccscsccc" + inkscape:connector-curvature="0" /> + sodipodi:nodetypes="cssscssssscscssssscscssssscscssssscscccssscssscscssssscscssscssscscssssscssssscsccc" + inkscape:connector-curvature="0" /> + sodipodi:nodetypes="cssssscscccscssssscscccssscssscscssssscssssscscssssscscssssscssssscscssssscscsssssccccscssssscsccc" + inkscape:connector-curvature="0" /> diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index a09cbb1..efe012e 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -36,6 +36,7 @@ #---------------------------------------------------------------------------- DEFAULT_DPI = 96 # 96 as of Inkscape 0.92 +MINIMUM_SIZE = 1e-5 # Minimum size kicad will render #---------------------------------------------------------------------------- @@ -123,6 +124,8 @@ def _get_fill_stroke( self, item ): # Give a default stroke width? stroke_width = self._convert_decimal_to_mm( 1 ) if self.use_mm else 1 + if stroke_width is None: + stroke_width = 0 return fill, stroke, stroke_width @@ -229,7 +232,7 @@ def _prune( self, items = None ): for name in self.layers.keys(): # if name == i_name[0] and i_name[0] != "": - if re.match( f'^{name}$', i_name[0]): + if re.match( '^{}$'.format(name), i_name[0]): if kept_layers.get(i_name[0]): kept_layers[i_name[0]].append(item.name) else: @@ -257,7 +260,7 @@ def _write_items( self, items, layer, flip = False ): self._write_items( item.items, layer, flip ) continue - if re.match(r"^Drill.\w+", layer): + if re.match(r"^Drill\.\w+", str(layer)): if isinstance(item, svg.Circle): self._write_thru_hole(item, layer) else: @@ -273,10 +276,14 @@ def _write_items( self, items, layer, flip = False ): ] fill, stroke, stroke_width = self._get_fill_stroke( item ) - fill = (False if layer == "Edge.Cuts" else fill) + if layer == "Edge.Cuts": + fill = False + stroke = True + stroke_width = MINIMUM_SIZE if stroke_width < MINIMUM_SIZE else stroke_width - fill = (True if re.match("^Keepout", layer) else fill) - stroke_width = (0.508 if re.match("^Keepout", layer) else stroke_width) + + fill = (True if re.match("^Keepout", str(layer)) else fill) + stroke_width = (0.508 if re.match("^Keepout", str(layer)) else stroke_width) for segment in segments: segment.process( self, flip, fill ) @@ -1138,7 +1145,7 @@ def _write_polygon_header( self, points, layer, stroke_width): create_pad = (self.convert_pads and l_name.find("Cu") == 2) or options.get("copper_pad") if stroke_width == 0: - stroke_width = 1e-5 #This is the smallest a pad can be and still be rendered in kicad + stroke_width = MINIMUM_SIZE if l_name == "Keepout": self._extra_indent = 1 @@ -1178,9 +1185,11 @@ def _write_polygon_header( self, points, layer, stroke_width): if options.get("pad_paste"): layer += " {}.Paste".format(l_name.split(".", 1)[0]) self._extra_indent = 1 + self._special_footer = " )\n (width {}) )\n ))".format( stroke_width ) + self.output_file.write( '''\n (pad "{0}" smd custom (at {1} {2}) (size {3:.6f} {3:.6f}) (layers {4}) (zone_connect 0) (options (clearance outline) (anchor circle)) @@ -1261,13 +1270,14 @@ def _write_thru_hole( self, circle, layer ): else: size = rad + center = self.transform_point(circle.center) self.output_file.write( '\n (pad "{0}" {1}thru_hole circle (at {2} {3}) (size {4} {4}) (drill {5}) (layers *.Mask{6}) {7})'.format( pad_number, #0 "" if plated else "np_", #1 - circle.center.x * self.scale_factor, #2 - circle.center.y * self.scale_factor, #3 + center.x, #2 + center.y, #3 size, #4 drill, #5 " *.Cu" if plated else "", #6 diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index bb398c9..dc5473f 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -1131,7 +1131,7 @@ def parse(self, elt, parent): } for style in self.style: if style in self.font_configs.keys() and self.style[style]: - self.font_configs[name] = self.style[style] + self.font_configs[style] = self.style[style] if isinstance(self.font_configs["font-size"], str): self.font_configs["font-size"] = float(self.font_configs["font-size"].strip("px")) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 59cb17e..3558f4f 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -19,6 +19,7 @@ from a svg object into a single continuous line ''' +import copy import logging from typing import List, Tuple @@ -294,7 +295,8 @@ def inline( self, segments: List[svg.Point] ) -> List[svg.Point]: if insertion is not None: insertions.append( insertion ) - points = self.points[ : ] + # Prevent returned points from affecting original object + points = copy.deepcopy(self.points) for insertion in insertions: @@ -305,9 +307,11 @@ def inline( self, segments: List[svg.Point] ) -> List[svg.Point]: points[ ip ].x == hole[ 0 ].x and points[ ip ].y == hole[ 0 ].y ): - points = points[:ip+1] + hole[ 1 : -1 ] + points[ip:] + # The point at the insertion point is duplicated so any action on that will affect both + points = points[:ip] + [copy.copy(points[ip])] + hole[ 1 : -1 ] + points[ip:] else: - points = points[:ip+1] + hole + points[ip:] + # The point at the insertion point is duplicated so any action on that will affect both + points = points[:ip] + [copy.copy(points[ip])] + hole + points[ip:] return points From 8e053869ceab7a03519f0e6bc560b513af5a9537 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 28 Jan 2022 19:49:13 -0700 Subject: [PATCH 119/151] Fix a bug with box sizing not updating --- svg2mod/svg2mod.py | 1 + 1 file changed, 1 insertion(+) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 3558f4f..4cae7ea 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -409,6 +409,7 @@ def process( self, transformer, flip, fill ): #) ) self.points = points + self.calc_bbox() #------------------------------------------------------------------------ From c8693388f9619aa52dc03a742eb7fa3a63d4fab0 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 31 Jan 2022 10:14:15 -0700 Subject: [PATCH 120/151] Fix for improper distinct detection --- svg2mod/exporter.py | 6 ++++++ svg2mod/svg2mod.py | 28 ++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index efe012e..41cfdaf 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -289,15 +289,21 @@ def _write_items( self, items, layer, flip = False ): segment.process( self, flip, fill ) if len( segments ) > 1: + # Sort segments in order of size segments.sort(key=lambda v: svg.Segment(v.bbox[0], v.bbox[1]).length(), reverse=True) + # Write all segments while len(segments) > 0: inlinable = [segments[0]] + + # Search to see if any paths are contained in the current shape for seg in segments[1:]: + # Contained in parent shape if not inlinable[0].are_distinct(seg): append = True if len(inlinable) > 1: for hole in inlinable[1:]: + # Contained in a hole. It is separate if not hole.are_distinct(seg): append = False break diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 4cae7ea..cff6e80 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -30,7 +30,6 @@ class LineSegment: '''Kicad can only draw straight lines. - This class can be type-cast from svg.geometry.Segment It is designed to have extra functions to help calculate intersections. ''' @@ -168,6 +167,8 @@ def q_next( self, q:svg.Point ): self.p = self.q self.q = q + #------------------------------------------------------------------------ + def __eq__(self, other): return ( isinstance(other, LineSegment) and @@ -175,7 +176,6 @@ def __eq__(self, other): other.q.x == self.q.x and other.q.y == self.q.y ) - #------------------------------------------------------------------------ #---------------------------------------------------------------------------- @@ -337,6 +337,7 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int intersections = 0 intersect_segs = [] + virt_line = LineSegment() # Check each segment of other hole for intersection: for point in self.points: @@ -354,8 +355,27 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int if get_points: intersect_segs.append((hole_segment.p, hole_segment.q)) else: - # If line_segment passes thru a point this prevents a second false positive - intersections += 0 if line_segment.on_line(hole_segment.q) else 1 + # If a point is on the line segment we need to see if the + # simplified "virtual" line crosses the line segment. + + # Set the endpoints if they are of the line segment + if line_segment.on_line(hole_segment.q): + if not line_segment.on_line(hole_segment.p): + virt_line.p = hole_segment.p + elif line_segment.on_line(hole_segment.p): + virt_line.q = hole_segment.q + + # No points are on the line segment + else: + intersections += 1 + virt_line = LineSegment() + + # The virtual line is complete check for intersections + if virt_line.p and virt_line.q: + if virt_line.intersects(line_segment): + intersections += 1 + virt_line = LineSegment() + elif get_points: return hole_segment.p, hole_segment.q else: From c2043927eb2869918e7482f1a4048719aeaafdc3 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 31 Jan 2022 14:38:26 -0700 Subject: [PATCH 121/151] Update downloads badge pypistats.org is being abused so this is a temporary solution to adapt another provider pepy.tech to be used with shields.io for-the-badge style --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 841ba8c..19180be 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version&style=for-the-badge)](https://pypi.org/project/svg2mod/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) -[![PyPI - Downloads](https://img.shields.io/pypi/dm/svg2mod?logo=python&logoColor=white&style=for-the-badge)](https://pypi.org/project/svg2mod/) +[![PyPI - Downloads](https://img.shields.io/endpoint?logoColor=red&url=https%3A%2F%2Fmikej.tech%2Fpepy.php%3Fproject%3Dsvg2mod)](https://pypi.org/project/svg2mod/) + [![GitHub Sponsors](https://img.shields.io/github/sponsors/sodium-hydrogen?logo=github&style=for-the-badge)](https://github.com/sponsors/Sodium-Hydrogen) [![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=purple&style=for-the-badge)](https://pypi.org/project/svg2mod/) From dee10687fa776f14513503b4e25a4cf37fff9310 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 31 Jan 2022 16:04:06 -0700 Subject: [PATCH 122/151] Added style inheritance, better stroke detection.. Also added better no fill for pretty formats. (need to test with kicad v5 still) --- svg2mod/exporter.py | 52 +++++++++++++++++++++++++++------------------ svg2mod/svg/svg.py | 42 ++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index 41cfdaf..81ee939 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -78,7 +78,7 @@ def _write_polygon_header( self, points, layer ):pass def _write_polygon( self, points, layer, fill, stroke, stroke_width ):pass @abstractmethod - def _write_polygon_footer( self, layer, stroke_width ):pass + def _write_polygon_footer( self, layer, stroke_width, fill=True):pass @abstractmethod def _write_polygon_point( self, point ):pass @@ -109,26 +109,24 @@ def _get_fill_stroke( self, item ): s = item.style - fill = False if s.get('fill') and s["fill"] == "none" else True + fill = False if not s.get('fill') or s["fill"] == "none" else True fill = fill if not s.get('fill-opacity') or float(s['fill-opacity']) != 0 else False - stroke = False if s.get('stroke') and s["stroke"] == "none" else True + stroke = False if not s.get('stroke') or s["stroke"] == "none" else True stroke = stroke if not s.get('stroke-opacity') or float(s['stroke-opacity']) != 0 else False - stroke_width = s["stroke-width"] if stroke and s.get('stroke-width') else None - - if stroke_width: - stroke_width *= self.scale_factor - - if stroke and stroke_width is None: - # Give a default stroke width? - stroke_width = self._convert_decimal_to_mm( 1 ) if self.use_mm else 1 + stroke_width = s["stroke-width"] * self.scale_factor if s.get('stroke-width') else MINIMUM_SIZE if stroke_width is None: stroke_width = 0 + # This should display something. + if not fill and not stroke: + stroke = True + stroke_width = stroke_width if stroke_width else MINIMUM_SIZE - return fill, stroke, stroke_width + # There should be no stroke_width if no stroke + return fill, stroke, stroke_width if stroke else 0 #------------------------------------------------------------------------ @@ -299,7 +297,7 @@ def _write_items( self, items, layer, flip = False ): # Search to see if any paths are contained in the current shape for seg in segments[1:]: # Contained in parent shape - if not inlinable[0].are_distinct(seg): + if fill and not inlinable[0].are_distinct(seg): append = True if len(inlinable) > 1: for hole in inlinable[1:]: @@ -375,7 +373,6 @@ def _write_module( self, front ): layer = self._get_layer_name( i_name, name, front ) - #print( " Writing layer: {}".format( name ) ) self._write_items( group.items, layer, not front ) self._write_module_footer( front ) @@ -642,7 +639,7 @@ def _write_polygon( self, points, layer, fill, stroke, stroke_width ): #------------------------------------------------------------------------ - def _write_polygon_footer( self, layer, stroke_width ): + def _write_polygon_footer( self, layer, stroke_width, fill=True ): pass @@ -1102,6 +1099,15 @@ def _write_polygon_filled( self, points, layer, stroke_width = 0): self._write_polygon_footer( layer, stroke_width ) + #------------------------------------------------------------------------ + + def _write_polygon_outline( self, points, layer, stroke_width = 0): + self._write_polygon_header( points, layer, stroke_width) + + for point in points: + self._write_polygon_point( point ) + + self._write_polygon_footer( layer, stroke_width, fill=False ) #------------------------------------------------------------------------ @@ -1123,14 +1129,18 @@ def _write_polygon( self, points, layer, fill, stroke, stroke_width ): #------------------------------------------------------------------------ - def _write_polygon_footer( self, layer, stroke_width ): + def _write_polygon_footer( self, layer, stroke_width, fill=True ): if self._special_footer: - self.output_file.write(self._special_footer) + self.output_file.write(self._special_footer.format( + layer.split(":", 1)[0], stroke_width, + " (fill none)" if not fill else "" + )) else: self.output_file.write( - " )\n (layer {})\n (width {})\n )".format( - layer.split(":", 1)[0], stroke_width + " )\n (layer {})\n (width {}){}\n )".format( + layer.split(":", 1)[0], stroke_width, + " (fill none)" if not fill else "" ) ) self._special_footer = "" @@ -1192,7 +1202,7 @@ def _write_polygon_header( self, points, layer, stroke_width): layer += " {}.Paste".format(l_name.split(".", 1)[0]) self._extra_indent = 1 - self._special_footer = " )\n (width {}) )\n ))".format( + self._special_footer = " )\n (width {}){{2}})\n ))".format( stroke_width ) @@ -1241,7 +1251,7 @@ def _write_polygon_segment( self, p, q, layer, stroke_width ): )""".format( p.x, p.y, q.x, q.y, - layer, + layer.split(':',1)[0], stroke_width, ) ) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index dc5473f..92e71b7 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -25,6 +25,7 @@ import json import logging import math +from multiprocessing import parent_process import operator import os import platform @@ -70,7 +71,7 @@ class Transformable: # This list is all styles that should have the transformation matrix applied transformable_styles = ["stroke-width"] - def __init__(self, elt=None): + def __init__(self, elt=None, parent_styles=None): # a 'Transformable' is represented as a list of Transformable items self.items = [] self.id = hex(id(self)) @@ -78,7 +79,7 @@ def __init__(self, elt=None): self.matrix = Matrix() self.scalex = 1 self.scaley = 1 - self.style = {} + self.style = {} if not parent_styles and not isinstance(parent_styles, dict) else parent_styles.copy() self.rotation = 0 self.viewport = Point(800, 600) # default viewport is 800x600 if elt is not None: @@ -96,12 +97,8 @@ def __init__(self, elt=None): if name in self.transformable_styles: value = list(re.search(r'(\d+\.?\d*)(\D+)?', value).groups()) self.style[name] = float(value[0]) - # if not value[1]: - # TODO verify that mm is the default value for more than stroke-width - # value[1] = "mm" if value[1] and value[1] not in unit_convert: logging.warning("Style '{}' has an unexpected unit: {}".format(style, value[1])) - # self.style[name] /= unit_convert[value[1]] else: self.style[name] = value @@ -199,9 +196,6 @@ def transform(self, matrix=None): else: matrix *= self.matrix self.transform_styles(matrix) - #print( "do transform: {}: {}".format( self.__class__.__name__, matrix ) ) - #print( "do transform: {}: {}".format( self, matrix ) ) - #traceback.print_stack() for x in self.items: x.transform(matrix) @@ -331,8 +325,8 @@ class Group(Transformable): # class Group handles the tag tag = 'g' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, *args, **kwargs): + Transformable.__init__(self, elt, *args, **kwargs) self.name = "" self.hidden = False @@ -368,7 +362,7 @@ def append(self, element): logging.debug('No handler for element %s' % elt.tag) continue # instantiate elt associated class (e.g. : item = Path(elt) - item = elt_class(elt) + item = elt_class(elt, parent_styles=self.style) # Apply group matrix to the newly created object # Actually, this is effectively done in Svg.__init__() through call to # self.transform(), so doing it here will result in the transformations @@ -457,8 +451,8 @@ class Path(Transformable): tag = 'path' COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, *args, **kwargs): + Transformable.__init__(self, elt, *args, **kwargs) if elt is not None: self.parse(elt.get('d')) @@ -645,8 +639,8 @@ class Ellipse(Transformable): # class Ellipse handles the tag tag = 'ellipse' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, *args, **kwargs): + Transformable.__init__(self, elt, *args, **kwargs) self.arc = False if elt is not None: self.center = Point(self.xlength(elt.get('cx')), @@ -911,11 +905,11 @@ class Circle(Ellipse): # class Circle handles the tag tag = 'circle' - def __init__(self, elt=None): + def __init__(self, elt=None, *args, **kwargs): if elt is not None: elt.set('rx', elt.get('r')) elt.set('ry', elt.get('r')) - Ellipse.__init__(self, elt) + Ellipse.__init__(self, elt, *args, **kwargs) def __repr__(self): return '' @@ -932,8 +926,8 @@ class Rect(Path): # class Rect handles the tag tag = 'rect' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, *args, **kwargs): + Transformable.__init__(self, elt, *args, **kwargs) if elt is not None: p = Point(self.xlength(elt.get('x')), self.ylength(elt.get('y'))) @@ -973,8 +967,8 @@ class Line(Transformable): # class Line handles the tag tag = 'line' - def __init__(self, elt=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, *args, **kwargs): + Transformable.__init__(self, elt, *args, **kwargs) if elt is not None: self.P1 = Point(self.xlength(elt.get('x1')), self.ylength(elt.get('y1'))) @@ -1049,8 +1043,8 @@ class Text(Transformable): "Windows": ["C:/Windows/Fonts", "~/AppData/Local/Microsoft/Windows/Fonts"] } - def __init__(self, elt=None, parent=None): - Transformable.__init__(self, elt) + def __init__(self, elt=None, parent=None, *args, **kwargs): + Transformable.__init__(self, elt, *args, **kwargs) self.bbox_points = [Point(0,0), Point(0,0)] self.paths = [] From 76e0d30dcd45bd63c2f322f0cc274954c8f689a6 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Tue, 1 Feb 2022 11:09:06 -0700 Subject: [PATCH 123/151] update downloads badge this removes my server from the flow for badge creation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 19180be..5cc3f72 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version&style=for-the-badge)](https://pypi.org/project/svg2mod/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) -[![PyPI - Downloads](https://img.shields.io/endpoint?logoColor=red&url=https%3A%2F%2Fmikej.tech%2Fpepy.php%3Fproject%3Dsvg2mod)](https://pypi.org/project/svg2mod/) +[![PyPI - Downloads](https://img.shields.io/badge/dynamic/xml?style=for-the-badge&color=green&label=downloads&query=%2F%2F%2A%5Blocal-name%28%29%20%3D%20%27text%27%5D%5Blast%28%29%5D&suffix=%2Fmonth&url=https%3A%2F%2Fstatic.pepy.tech%2Fbadge%2Fsvg2mod%2Fmonth)](https://pypi.org/project/svg2mod/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/sodium-hydrogen?logo=github&style=for-the-badge)](https://github.com/sponsors/Sodium-Hydrogen) From f5b3c0573b7d5524c0d597dc0670db25a46dab7b Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Tue, 1 Feb 2022 22:14:36 -0700 Subject: [PATCH 124/151] Force layer option More bug fixes and updated documentation --- README.md | 9 +++------ svg2mod/cli.py | 31 ++++++++++++++++--------------- svg2mod/exporter.py | 43 ++++++++++++++++++++++++------------------- svg2mod/importer.py | 32 ++++++++++++++++++++------------ svg2mod/svg2mod.py | 10 ---------- 5 files changed, 63 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 19180be..1c9f52c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version&style=for-the-badge)](https://pypi.org/project/svg2mod/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) -[![PyPI - Downloads](https://img.shields.io/endpoint?logoColor=red&url=https%3A%2F%2Fmikej.tech%2Fpepy.php%3Fproject%3Dsvg2mod)](https://pypi.org/project/svg2mod/) +[![PyPI - Downloads](https://img.shields.io/badge/dynamic/xml?style=for-the-badge&color=green&label=downloads&query=%2F%2F%2A%5Blocal-name%28%29%20%3D%20%27text%27%5D%5Blast%28%29%5D&suffix=%2Fmonth&url=https%3A%2F%2Fstatic.pepy.tech%2Fbadge%2Fsvg2mod%2Fmonth)](https://pypi.org/project/svg2mod/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/sodium-hydrogen?logo=github&style=for-the-badge)](https://github.com/sponsors/Sodium-Hydrogen) @@ -81,13 +81,12 @@ optional arguments: svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. This is so it can associate inkscape layers with kicad layers * 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. -* Paths are fully supported Rect are partially supported. +* Most elements are fully supported. * A path may have an outline and a fill. (Colors will be ignored.) * A path may have holes, defined by interior segments within the path (see included examples). * 100% Transparent fills and strokes with be ignored. - * Rect supports rotations, but not corner radii. * Text Elements are partially supported -* 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. +* Groups may be used. Styles applied to groups (e.g., stroke-width) are applied to contained drawing elements. * Layers must be named to match the target in kicad. The supported layers are listed below. They will be ignored otherwise. * __If there is an issue parsing an inkscape object or stroke convert it to a path.__ * __Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these elements into paths that will work.__ @@ -168,5 +167,3 @@ Supported Arguments: [^3]: Only works in Kicad versions >= v6. [^4]: The * can be { *, F, B, I } or any combination like FB or BI. These options are for Front, Back, and Internal. - - diff --git a/svg2mod/cli.py b/svg2mod/cli.py index 552d603..94bd410 100644 --- a/svg2mod/cli.py +++ b/svg2mod/cli.py @@ -58,9 +58,9 @@ def main(): logging.getLogger("unfiltered").setLevel(logging.INFO) # This can be used sparingly as follows: - ########## + #--------- # logging.getLogger("unfiltered").info("Message Here") - ########## + #--------- if args.list_fonts: fonts = svg.Text.load_system_fonts() @@ -84,19 +84,14 @@ def main(): logging.critical("Error: decimal units only allowed with legacy output type") sys.exit( -1 ) - #if args.include_reverse: - #print( - #"Warning: reverse footprint not supported or required for" + - #" pretty output format" - #) - try: # Import the SVG: imported = Svg2ModImport( args.input_file_name, args.module_name, args.module_value, - args.ignore_hidden_layers, + args.ignore_hidden, + args.force_layer ) # Pick an output file name if none was provided: @@ -144,8 +139,6 @@ def main(): except Exception as e: raise e - #print( e.message ) - #exported = None # Write the module file: if exported is None: @@ -240,13 +233,22 @@ def get_arguments(): parser.add_argument( '-x', '--exclude-hidden', - dest = 'ignore_hidden_layers', + dest = 'ignore_hidden', action = 'store_const', const = True, - help = "Do not export hidden layers", + help = "Do not export hidden objects", default = False, ) + parser.add_argument( + '--force', '--force-layer', + type = str, + dest = 'force_layer', + metavar = 'LAYER', + help = "Force everything into the single provided layer", + default = None, + ) + parser.add_argument( '-d', '--dpi', type = int, @@ -271,7 +273,7 @@ def get_arguments(): dest = 'precision', metavar = 'PRECISION', help = "Smoothness for approximating curves with line segments. Input is the approximate length for each line segment in SVG pixels (float)", - default = 10.0, + default = 5.0, ) parser.add_argument( '--format', @@ -336,5 +338,4 @@ def get_arguments(): if __name__ == "__main__": main() - #---------------------------------------------------------------------------- diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index 81ee939..35a6a0f 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -121,7 +121,7 @@ def _get_fill_stroke( self, item ): stroke_width = 0 # This should display something. - if not fill and not stroke: + if not self.imported.ignore_hidden and not fill and not stroke: stroke = True stroke_width = stroke_width if stroke_width else MINIMUM_SIZE @@ -217,6 +217,7 @@ def _prune( self, items = None ): for name in self.layer_map.keys(): self.layers[ name ] = [] + items = self.imported.svg.items self.imported.svg.items = [] @@ -235,7 +236,6 @@ def _prune( self, items = None ): kept_layers[i_name[0]].append(item.name) else: kept_layers[i_name[0]] = [item.name] - #logging.getLogger("unfiltered").info( "Found SVG layer: {}".format( item.name ) ) self.imported.svg.items.append( item ) self.layers[name].append((i_name, item)) @@ -589,13 +589,13 @@ def _write_module_header( T0 0 {1} {2} {2} 0 {3} N I 21 "{0}" T1 0 {5} {2} {2} 0 {3} N I 21 "{4}" """.format( - self._get_module_name( front ), - reference_y, - label_size, - label_pen, - self.imported.module_value, - value_y, - 15, # Seems necessary + self._get_module_name( front ), #0 + reference_y, #1 + label_size, #2 + label_pen, #3 + self.imported.module_value, #4 + value_y, #5 + 15, # Seems necessary #6 ) ) @@ -1202,14 +1202,11 @@ def _write_polygon_header( self, points, layer, stroke_width): layer += " {}.Paste".format(l_name.split(".", 1)[0]) self._extra_indent = 1 - self._special_footer = " )\n (width {}){{2}})\n ))".format( - stroke_width - ) + self._special_footer = "\n )" self.output_file.write( '''\n (pad "{0}" smd custom (at {1} {2}) (size {3:.6f} {3:.6f}) (layers {4}) (zone_connect 0) - (options (clearance outline) (anchor circle)) - (primitives\n (gr_poly (pts \n'''.format( + (options (clearance outline) (anchor circle))'''.format( pad_number, #0 points[0].x, #1 points[0].y, #2 @@ -1217,11 +1214,19 @@ def _write_polygon_header( self, points, layer, stroke_width): layer, #4 ) ) - originx = points[0].x - originy = points[0].y - for point in points: - point.x = point.x-originx - point.y = point.y-originy + # Pads primitives with 2 or less points crash kicad + if len(points) >= 2: + self.output_file.write('''\n (primitives\n (gr_poly (pts \n''') + self._special_footer = " )\n (width {}){{2}})\n ))".format(stroke_width) + + originx = points[0].x + originy = points[0].y + for point in points: + point.x = point.x-originx + point.y = point.y-originy + else: + for point in points[:]: + points.remove(point) else: self.output_file.write( "\n (fp_poly\n (pts \n" ) diff --git a/svg2mod/importer.py b/svg2mod/importer.py index db25080..09a3419 100644 --- a/svg2mod/importer.py +++ b/svg2mod/importer.py @@ -36,33 +36,41 @@ def _prune_hidden( self, items = None ): if items is None: items = self.svg.items - self.svg.items = [] + # self.svg.items = [] - for item in items: + for item in items[:]: - if not isinstance( item, svg.Group ): - continue + # if not isinstance( item, svg.Group ): + # continue - if item.hidden : - logging.warning("Ignoring hidden SVG layer: {}".format( item.name ) ) - elif item.name != "": - self.svg.items.append( item ) + if hasattr(item, "hidden") and item.hidden : + if item.name: + logging.warning("Ignoring hidden SVG item: {}".format( item.name ) ) + items.remove(item) + # elif item.name != "": + # self.svg.items.append( item ) - if item.items: + if hasattr(item, "items") and item.items: self._prune_hidden( item.items ) - def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", ignore_hidden_layers=False ): + def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", ignore_hidden=False, force_layer=None): self.file_name = file_name self.module_name = module_name self.module_value = module_value + self.ignore_hidden = ignore_hidden if file_name: logging.getLogger("unfiltered").info( "Parsing SVG..." ) - self.svg = svg.parse( file_name) + self.svg = svg.parse( file_name ) logging.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) - if ignore_hidden_layers: + if force_layer: + new_layer = svg.Group() + new_layer.name = force_layer + new_layer.items = self.svg.items[:] + self.svg.items = [new_layer] + if self.ignore_hidden: self._prune_hidden() diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index cff6e80..0b2da6e 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -411,10 +411,6 @@ def process( self, transformer, flip, fill ): points[ 0 ].x != points[ -1 ].x or points[ 0 ].y != points[ -1 ].y ): - #print( "Warning: Closing polygon. start=({}, {}) end=({}, {})".format( - #points[ 0 ].x, points[ 0 ].y, - #points[ -1 ].x, points[ -1 ].y, - #) ) if fill: points.append( svg.Point( @@ -422,12 +418,6 @@ def process( self, transformer, flip, fill ): points[ 0 ].y, ) ) - #else: - #print( "Polygon closed: start=({}, {}) end=({}, {})".format( - #points[ 0 ].x, points[ 0 ].y, - #points[ -1 ].x, points[ -1 ].y, - #) ) - self.points = points self.calc_bbox() From 3dda55d514369154f896f044dcef5eb6b65c7523 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Tue, 1 Feb 2022 22:20:30 -0700 Subject: [PATCH 125/151] Remove commented code --- svg2mod/importer.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/svg2mod/importer.py b/svg2mod/importer.py index 09a3419..c671d64 100644 --- a/svg2mod/importer.py +++ b/svg2mod/importer.py @@ -36,19 +36,13 @@ def _prune_hidden( self, items = None ): if items is None: items = self.svg.items - # self.svg.items = [] for item in items[:]: - # if not isinstance( item, svg.Group ): - # continue - if hasattr(item, "hidden") and item.hidden : if item.name: logging.warning("Ignoring hidden SVG item: {}".format( item.name ) ) items.remove(item) - # elif item.name != "": - # self.svg.items.append( item ) if hasattr(item, "items") and item.items: self._prune_hidden( item.items ) From 9fd6afd09de9c3d7bab6a060148675d523aa60c9 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Tue, 1 Feb 2022 22:22:18 -0700 Subject: [PATCH 126/151] Update Readme.md --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1c9f52c..04ef101 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,8 @@ If you have a project you are proud of please post about it on our ```text usage: svg2mod [-h] [-i FILENAME] [-o FILENAME] [-c] [-P] [-v] [--debug] [-x] - [-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] - [--name NAME] [--units UNITS] [--value VALUE] [-F DEFAULT_FONT] - [-l] + [--force LAYER] [-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] + [--name NAME] [--units UNITS] [--value VALUE] [-F DEFAULT_FONT] [-l] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -56,14 +55,16 @@ optional arguments: -P, --convert-pads Convert any artwork on Cu layers to pads -v, --verbose Print more verbose messages --debug Print debug level messages - -x, --exclude-hidden Do not export hidden layers + -x, --exclude-hidden Do not export hidden objects + --force LAYER, --force-layer LAYER + Force everything into the single provided layer -d DPI, --dpi DPI DPI of the SVG file (int) -f FACTOR, --factor FACTOR Scale paths by this factor -p PRECISION, --precision PRECISION - Smoothness for approximating curves with line - segments. Input is the approximate length for each - line segment in SVG pixels (float) + Smoothness for approximating curves with line segments. Input + is the approximate length for each line segment in SVG pixels + (float) --format FORMAT Output module file format (legacy|pretty) --name NAME, --module-name NAME Base name of the module @@ -71,8 +72,8 @@ optional arguments: --value VALUE, --module-value VALUE Value of the module -F DEFAULT_FONT, --default-font DEFAULT_FONT - Default font to use if the target font in a text - element cannot be found + Default font to use if the target font in a text element cannot + be found -l, --list-fonts List all fonts that can be found in common locations ``` From 9df71262356e3fbe70d5fae33a858e45100c9e14 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 2 Feb 2022 11:05:38 -0700 Subject: [PATCH 127/151] Re-add support for pretty pre kicad v6 --- .github/workflows/python-package.yml | 4 +- README.md | 17 ++- svg2mod/cli.py | 12 +- svg2mod/exporter.py | 168 ++++++++++++++++++++++----- svg2mod/importer.py | 4 +- 5 files changed, 158 insertions(+), 47 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 6b713bb..73ffe7b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -38,7 +38,9 @@ jobs: - name: Run svg tests run: | svg2mod -i examples/svg2mod.svg -o output.mod -x -c -P --name TEST --value VALUE -f 2 -p 1 --format legacy --units mm -d 300 --debug + svg2mod -i examples/svg2mod.svg --format legacy --force F.Cu + svg2mod -i examples/svg2mod.svg --format legacy -o output.mod + svg2mod -i examples/svg2mod.svg --format pretty --debug --force F.Cu -x svg2mod -i examples/svg2mod.svg --debug - svg2mod -i examples/svg2mod.svg --format legacy svg2mod -i examples/svg2mod.svg svg2mod -l diff --git a/README.md b/README.md index 04ef101..99d6b85 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ If you have a project you are proud of please post about it on our ## Usage ```text -usage: svg2mod [-h] [-i FILENAME] [-o FILENAME] [-c] [-P] [-v] [--debug] [-x] - [--force LAYER] [-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] - [--name NAME] [--units UNITS] [--value VALUE] [-F DEFAULT_FONT] [-l] +usage: svg2mod [-h] [-i FILENAME] [-o FILENAME] [-c] [-P] [-v] [--debug] [-x] [--force LAYER] +[-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] [--name NAME] [--units UNITS] + [--value VALUE] [-F DEFAULT_FONT] [-l] Convert Inkscape SVG drawings to KiCad footprint modules. @@ -62,18 +62,17 @@ optional arguments: -f FACTOR, --factor FACTOR Scale paths by this factor -p PRECISION, --precision PRECISION - Smoothness for approximating curves with line segments. Input - is the approximate length for each line segment in SVG pixels - (float) - --format FORMAT Output module file format (legacy|pretty) + Smoothness for approximating curves with line segments. Input is the + approximate length for each line segment in SVG pixels (float) + --format FORMAT Output module file format (legacy|pretty|latest). 'latest' introduces + features used in kicad >= 6 --name NAME, --module-name NAME Base name of the module --units UNITS Output units, if output format is legacy (decimal|mm) --value VALUE, --module-value VALUE Value of the module -F DEFAULT_FONT, --default-font DEFAULT_FONT - Default font to use if the target font in a text element cannot - be found + Default font to use if the target font in a text element cannot be found -l, --list-fonts List all fonts that can be found in common locations ``` diff --git a/svg2mod/cli.py b/svg2mod/cli.py index 94bd410..61c93b1 100644 --- a/svg2mod/cli.py +++ b/svg2mod/cli.py @@ -28,7 +28,7 @@ import svg2mod.coloredlogger as coloredlogger from svg2mod import svg -from svg2mod.exporter import (DEFAULT_DPI, Svg2ModExportLegacy, +from svg2mod.exporter import (DEFAULT_DPI, Svg2ModExportLatest, Svg2ModExportLegacy, Svg2ModExportLegacyUpdater, Svg2ModExportPretty) from svg2mod.importer import Svg2ModImport @@ -75,7 +75,7 @@ def main(): if args.default_font: svg.Text.default_font = args.default_font - pretty = args.format == 'pretty' + pretty = args.format in ['pretty','latest'] use_mm = args.units == 'mm' if pretty: @@ -111,7 +111,7 @@ def main(): # Create an exporter: if pretty: - exported = Svg2ModExportPretty( + exported = (Svg2ModExportPretty if args.format == "pretty" else Svg2ModExportLatest)( imported, args.output_file_name, args.center, @@ -280,9 +280,9 @@ def get_arguments(): type = str, dest = 'format', metavar = 'FORMAT', - choices = [ 'legacy', 'pretty' ], - help = "Output module file format (legacy|pretty)", - default = 'pretty', + choices = [ 'legacy', 'pretty', 'latest'], + help = "Output module file format (legacy|pretty|latest). 'latest' introduces features used in kicad >= 6", + default = 'latest', ) parser.add_argument( diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index 35a6a0f..75f2610 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -232,6 +232,10 @@ def _prune( self, items = None ): for name in self.layers.keys(): # if name == i_name[0] and i_name[0] != "": if re.match( '^{}$'.format(name), i_name[0]): + # Don't add empty groups to the list of valid items + if isinstance(item, svg.Group) and not item.items: + break + if kept_layers.get(i_name[0]): kept_layers[i_name[0]].append(item.name) else: @@ -247,6 +251,13 @@ def _prune( self, items = None ): logging.getLogger("unfiltered").info( "Found SVG layer: {}".format( kept ) ) logging.debug( " Detailed SVG layers: {}".format( ", ".join(kept_layers[kept]) ) ) + # There are no elements to write so don't write + for name in self.layers: + if self.layers[name]: + break + else: + raise Exception("Not writing empty file. No valid items found.") + #------------------------------------------------------------------------ @@ -914,7 +925,7 @@ def _write_module_header( class Svg2ModExportPretty( Svg2ModExport ): ''' This provides functionality for the - newer kicad "pretty" footprint file formats. + older kicad "pretty" footprint file formats. It is a child of Svg2ModExport. ''' @@ -941,12 +952,16 @@ class Svg2ModExportPretty( Svg2ModExport ): 'B.Fab' : "B.Fab", 'Drill.Cu': "Drill.Cu", 'Drill.Mech': "Drill.Mech", - r'\S+\.Keepout': "Keepout" } keepout_allowed = ['tracks','vias','pads','copperpour','footprints'] + # Breaking changes where introduced in kicad v6 + # This variable disables the drill breaking changes for v5 support + _drill_inner_layers = False + + #------------------------------------------------------------------------ def __init__(self, *args, **kwargs): @@ -987,6 +1002,7 @@ def _get_layer_name( self, item_name, name, front ): if len(item_name) == 2 and item_name[1]: for arg in item_name[1].split(';'): arg = arg.strip(' ,:') + # This is used in Svg2ModExportLatest as it is a breaking change if name == "Keepout" and re.match(r'^allowed:\w+', arg, re.I): attrs["allowed"] = [] for allowed in arg.lower().split(":", 1)[1].split(','): @@ -1101,16 +1117,6 @@ def _write_polygon_filled( self, points, layer, stroke_width = 0): #------------------------------------------------------------------------ - def _write_polygon_outline( self, points, layer, stroke_width = 0): - self._write_polygon_header( points, layer, stroke_width) - - for point in points: - self._write_polygon_point( point ) - - self._write_polygon_footer( layer, stroke_width, fill=False ) - - #------------------------------------------------------------------------ - def _write_polygon( self, points, layer, fill, stroke, stroke_width ): if fill: @@ -1131,16 +1137,15 @@ def _write_polygon( self, points, layer, fill, stroke, stroke_width ): def _write_polygon_footer( self, layer, stroke_width, fill=True ): + #Format option #2 is expected, but only used in Svg2ModExportLatest if self._special_footer: self.output_file.write(self._special_footer.format( - layer.split(":", 1)[0], stroke_width, - " (fill none)" if not fill else "" + layer.split(":", 1)[0], stroke_width, "" #2 )) else: self.output_file.write( " )\n (layer {})\n (width {}){}\n )".format( - layer.split(":", 1)[0], stroke_width, - " (fill none)" if not fill else "" + layer.split(":", 1)[0], stroke_width, "" #3 ) ) self._special_footer = "" @@ -1246,21 +1251,54 @@ def _write_polygon_point( self, point ): #------------------------------------------------------------------------ def _write_polygon_segment( self, p, q, layer, stroke_width ): + l_name = layer + options = {} + try: + l_name, options = layer.split(":", 1) + options = json.loads(options) + except ValueError:pass - self.output_file.write( - """\n (fp_line - (start {} {}) - (end {} {}) - (layer {}) - (width {}) - )""".format( - p.x, p.y, - q.x, q.y, - layer.split(':',1)[0], - stroke_width, + create_pad = (self.convert_pads and l_name.find("Cu") == 2) or options.get("copper_pad") + + if stroke_width == 0: + stroke_width = MINIMUM_SIZE + + if create_pad: + pad_number = "" if not options.get("copper_pad") or isinstance(options["copper_pad"], bool) else str(options.get("copper_pad")) + layer = l_name + if options.get("pad_mask"): + layer += " {}.Mask".format(l_name.split(".", 1)[0]) + if options.get("pad_paste"): + layer += " {}.Paste".format(l_name.split(".", 1)[0]) + + # There are major performance issues when multiple line primitives are in the same pad + self.output_file.write( '''\n (pad "{0}" smd custom (at {1} {2}) (size {3:.6f} {3:.6f}) (layers {4}) + (zone_connect 0) + (options (clearance outline) (anchor circle)) + (primitives\n (gr_line (start 0 0) (end {5} {6}) (width {3})) + ))'''.format( + pad_number, #0 + p.x, #1 + p.y, #2 + stroke_width, #3 + layer, #4 + q.x - p.x, #5 + q.y - p.y, #6 + ) ) - ) + else: + self.output_file.write( + """\n (fp_line + (start {} {}) (end {} {}) + (layer {}) (width {}) + )""".format( + p.x, p.y, + q.x, q.y, + layer.split(':',1)[0], + stroke_width, + ) + ) #------------------------------------------------------------------------ @@ -1302,10 +1340,82 @@ def _write_thru_hole( self, circle, layer ): size, #4 drill, #5 " *.Cu" if plated else "", #6 - "(remove_unused_layers) (keep_end_layers)" if plated else "", #7 + "(remove_unused_layers) (keep_end_layers)" if plated and self._drill_inner_layers else "", #7 ) ) #------------------------------------------------------------------------ #---------------------------------------------------------------------------- + + +class Svg2ModExportLatest(Svg2ModExportPretty): + ''' This provides functionality for the newer kicad + "pretty" footprint file formats introduced in kicad v6. + It is a child of Svg2ModExport. + ''' + + layer_map = { + #'inkscape-name' : kicad-name, + '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", + 'Drill.Cu': "Drill.Cu", + 'Drill.Mech': "Drill.Mech", + r'\S+\.Keepout': "Keepout" + } + + # Breaking changes where introduced in kicad v6 + # This variable enables the drill breaking changes for v5 support + _drill_inner_layers = True + + #------------------------------------------------------------------------ + + def _write_polygon_outline( self, points, layer, stroke_width = 0): + self._write_polygon_header( points, layer, stroke_width) + + for point in points: + self._write_polygon_point( point ) + + self._write_polygon_footer( layer, stroke_width, fill=False ) + + #------------------------------------------------------------------------ + + def _write_polygon_footer( self, layer, stroke_width, fill=True ): + + if self._special_footer: + self.output_file.write(self._special_footer.format( + layer.split(":", 1)[0], stroke_width, + " (fill none)" if not fill else "" + )) + else: + self.output_file.write( + " )\n (layer {})\n (width {}){}\n )".format( + layer.split(":", 1)[0], stroke_width, + " (fill none)" if not fill else "" + ) + ) + self._special_footer = "" + self._extra_indent = 0 + + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- diff --git a/svg2mod/importer.py b/svg2mod/importer.py index c671d64..67be689 100644 --- a/svg2mod/importer.py +++ b/svg2mod/importer.py @@ -39,8 +39,8 @@ def _prune_hidden( self, items = None ): for item in items[:]: - if hasattr(item, "hidden") and item.hidden : - if item.name: + if (hasattr(item, "hidden") and item.hidden ) or (hasattr(item, "style") and item.style.get("display") == "none"): + if hasattr(item, "name") and item.name: logging.warning("Ignoring hidden SVG item: {}".format( item.name ) ) items.remove(item) From 153b89bc282ad0d1bff01c574403eacd00685139 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 2 Feb 2022 14:07:15 -0700 Subject: [PATCH 128/151] Switch to stop using the 'root' logger --- README.md | 50 ++++++++++++++++++++-------------------- svg2mod/cli.py | 34 +++++++++++---------------- svg2mod/coloredlogger.py | 36 ++++++++++++++++++++++++++++- svg2mod/exporter.py | 31 +++++++++++++------------ svg2mod/importer.py | 12 ++++++---- svg2mod/svg/svg.py | 30 ++++++++++++------------ svg2mod/svg2mod.py | 7 +++--- 7 files changed, 115 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 99d6b85..91087a0 100644 --- a/README.md +++ b/README.md @@ -95,30 +95,30 @@ svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain This supports the layers listed below. They are the same in inkscape and kicad: -| KiCad layer(s) | KiCad legacy | KiCad pretty | -|:------------------------:|:------------:|:------------:| -| F.Cu [^1] | Yes | Yes | -| B.Cu [^1] | Yes | Yes | -| F.Adhes | Yes | Yes | -| B.Adhes | Yes | Yes | -| F.Paste | Yes | Yes | -| B.Paste | Yes | Yes | -| F.SilkS | Yes | Yes | -| B.SilkS | Yes | Yes | -| F.Mask | Yes | Yes | -| B.Mask | Yes | Yes | -| Dwgs.User | Yes | Yes | -| Cmts.User | Yes | Yes | -| Eco1.User | Yes | Yes | -| Eco2.User | Yes | Yes | -| Edge.Cuts | Yes | Yes | -| F.Fab | -- | Yes | -| B.Fab | -- | Yes | -| F.CrtYd | -- | Yes | -| B.CrtYd | -- | Yes | -| Drill.Cu [^1] [^2] | -- | Yes | -| Drill.Mech [^1] [^2] | -- | Yes | -| *.Keepout [^1] [^3] [^4] | -- | Yes | +| KiCad layer(s) | KiCad legacy | KiCad pretty | +|:--------------------:|:------------:|:------------:| +| F.Cu [^1] | Yes | Yes | +| B.Cu [^1] | Yes | Yes | +| F.Adhes | Yes | Yes | +| B.Adhes | Yes | Yes | +| F.Paste | Yes | Yes | +| B.Paste | Yes | Yes | +| F.SilkS | Yes | Yes | +| B.SilkS | Yes | Yes | +| F.Mask | Yes | Yes | +| B.Mask | Yes | Yes | +| Dwgs.User | Yes | Yes | +| Cmts.User | Yes | Yes | +| Eco1.User | Yes | Yes | +| Eco2.User | Yes | Yes | +| Edge.Cuts | Yes | Yes | +| F.Fab | -- | Yes | +| B.Fab | -- | Yes | +| F.CrtYd | -- | Yes | +| B.CrtYd | -- | Yes | +| Drill.Cu [^1] [^2] | -- | Yes | +| Drill.Mech [^1] [^2] | -- | Yes | +| *.Keepout [^1] [^4] | -- | Yes [^3] | Note: If you have a layer `F.Cu`, all of its sub-layers will be treated as `F.Cu` regardless of their names. @@ -164,6 +164,6 @@ Supported Arguments: [^2]: Drills can only be svg circle objects. The stroke width in `Drill.Cu` is the pad size and the fill is the drill size. -[^3]: Only works in Kicad versions >= v6. +[^3]: Only works in Kicad versions >= v6 (`--format latest`). [^4]: The * can be { *, F, B, I } or any combination like FB or BI. These options are for Front, Back, and Internal. diff --git a/svg2mod/cli.py b/svg2mod/cli.py index 61c93b1..8c2b90f 100644 --- a/svg2mod/cli.py +++ b/svg2mod/cli.py @@ -27,11 +27,14 @@ import traceback import svg2mod.coloredlogger as coloredlogger +from svg2mod.coloredlogger import logger, unfiltered_logger from svg2mod import svg -from svg2mod.exporter import (DEFAULT_DPI, Svg2ModExportLatest, Svg2ModExportLegacy, - Svg2ModExportLegacyUpdater, Svg2ModExportPretty) +from svg2mod.exporter import (DEFAULT_DPI, Svg2ModExportLatest, + Svg2ModExportLegacy, Svg2ModExportLegacyUpdater, + Svg2ModExportPretty) from svg2mod.importer import Svg2ModImport +#---------------------------------------------------------------------------- def main(): '''This function handles the scripting package calls. @@ -44,33 +47,24 @@ def main(): # Setup root logger to use terminal colored outputs as well as stdout and stderr - coloredlogger.split_logger(logging.root) + coloredlogger.split_logger(logger) if args.verbose_print: - logging.root.setLevel(logging.INFO) + logger.setLevel(logging.INFO) elif args.debug_print: - logging.root.setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) else: - logging.root.setLevel(logging.WARNING) - - # Add a second logger that will bypass the log level and output anyway - # It is a good practice to send only messages level INFO via this logger - logging.getLogger("unfiltered").setLevel(logging.INFO) - - # This can be used sparingly as follows: - #--------- - # logging.getLogger("unfiltered").info("Message Here") - #--------- + logger.setLevel(logging.WARNING) if args.list_fonts: fonts = svg.Text.load_system_fonts() - logging.getLogger("unfiltered").info("Font Name: list of supported styles.") + unfiltered_logger.info("Font Name: list of supported styles.") for font in fonts: fnt_text = f" {font}:" for styles in fonts[font]: fnt_text += f" {styles}," fnt_text = fnt_text.strip(",") - logging.getLogger("unfiltered").info(fnt_text) + unfiltered_logger.info(fnt_text) sys.exit(0) if args.default_font: svg.Text.default_font = args.default_font @@ -81,7 +75,7 @@ def main(): if pretty: if not use_mm: - logging.critical("Error: decimal units only allowed with legacy output type") + logger.critical("Error: decimal units only allowed with legacy output type") sys.exit( -1 ) try: @@ -161,7 +155,7 @@ def main(): if args.debug_print: traceback.print_exc() else: - logging.critical(f'Unhandled exception (Exiting)\n {type(e).__name__}: {e} ') + logger.critical(f'Unhandled exception (Exiting)\n {type(e).__name__}: {e} ') exit(-1) #---------------------------------------------------------------------------- @@ -331,8 +325,6 @@ def get_arguments(): return parser.parse_args(), parser - #------------------------------------------------------------------------ - #---------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index 5a588c4..a8d155d 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -21,6 +21,30 @@ import logging import sys +#---------------------------------------------------------------------------- + +#Setup and configure svg2mod and svg2mod-unfiltered loggers + +logger = logging.getLogger("svg2mod") +unfiltered_logger = logging.getLogger("svg2mod-unfiltered") +_sh = logging.StreamHandler(sys.stdout) + +logger.addHandler(_sh) +unfiltered_logger.addHandler(_sh) + +logger.setLevel(logging.DEBUG) + +# Add a second logger that will bypass the log level and output anyway +# It is a good practice to send only messages level INFO via this logger +unfiltered_logger.setLevel(logging.INFO) + +# This can be used sparingly as follows: +#--------- +# unfiltered_logger.info("Message Here") +#--------- + + +#---------------------------------------------------------------------------- class Formatter(logging.Formatter): '''Extend formatter to add colored output functionality ''' @@ -35,9 +59,13 @@ class Formatter(logging.Formatter): } reset = "\033[0m" # Reset the terminal back to default color/emphasis + #------------------------------------------------------------------------ + def __init__(self, fmt="%(message)s", datefmt=None, style="%"): super().__init__(fmt, datefmt, style) + #------------------------------------------------------------------------ + def format(self, record): '''Overwrite the format function. This saves the original style, overwrites it to support @@ -52,10 +80,16 @@ def format(self, record): self._style._fmt = fmt_org return result + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + def split_logger(logger, formatter=Formatter(), brkpoint=logging.WARNING): '''This will split logging messages at the specified break point. Anything higher will be sent to sys.stderr and everything else to sys.stdout ''' + for hndl in logger.handlers: + logger.removeHandler(hndl) hdlrerr = logging.StreamHandler(sys.stderr) hdlrerr.addFilter(lambda msg: brkpoint <= msg.levelno) @@ -68,4 +102,4 @@ def split_logger(logger, formatter=Formatter(), brkpoint=logging.WARNING): logger.addHandler(hdlrerr) logger.addHandler(hdlrout) - +#---------------------------------------------------------------------------- \ No newline at end of file diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index 75f2610..30b9fca 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -23,13 +23,13 @@ import datetime import io import json -import logging import os import re import time from abc import ABC, abstractmethod from svg2mod import svg +from svg2mod.coloredlogger import logger, unfiltered_logger from svg2mod.importer import Svg2ModImport from svg2mod.svg2mod import PolygonSegment @@ -47,6 +47,8 @@ class Svg2ModExport(ABC): example: pretty, legacy ''' + #------------------------------------------------------------------------ + @property @abstractmethod def layer_map(self ): @@ -248,8 +250,8 @@ def _prune( self, items = None ): self._prune( item.items ) for kept in sorted(kept_layers.keys()): - logging.getLogger("unfiltered").info( "Found SVG layer: {}".format( kept ) ) - logging.debug( " Detailed SVG layers: {}".format( ", ".join(kept_layers[kept]) ) ) + unfiltered_logger.info( "Found SVG layer: {}".format( kept ) ) + logger.debug( " Detailed names: [{}]".format( ", ".join(kept_layers[kept]) ) ) # There are no elements to write so don't write for name in self.layers: @@ -258,7 +260,6 @@ def _prune( self, items = None ): else: raise Exception("Not writing empty file. No valid items found.") - #------------------------------------------------------------------------ def _write_items( self, items, layer, flip = False ): @@ -273,7 +274,7 @@ def _write_items( self, items, layer, flip = False ): if isinstance(item, svg.Circle): self._write_thru_hole(item, layer) else: - logging.warning( "Non Circle SVG element in drill layer: {}".format(item.__class__.__name__)) + logger.warning( "Non Circle SVG element in drill layer: {}".format(item.__class__.__name__)) elif isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text)): @@ -324,7 +325,7 @@ def _write_items( self, items, layer, flip = False ): elif len(inlinable) > 0: points = inlinable[ 0 ].points - logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) + logger.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) self._write_polygon( points, layer, fill, stroke, stroke_width @@ -336,16 +337,16 @@ def _write_items( self, items, layer, flip = False ): if len ( segments ) == 1: - logging.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) + logger.info( " Writing {} with {} points".format(item.__class__.__name__, len( points ) )) self._write_polygon( points, layer, fill, stroke, stroke_width ) else: - logging.info( " Skipping {} with 0 points".format(item.__class__.__name__)) + logger.info( " Skipping {} with 0 points".format(item.__class__.__name__)) else: - logging.warning( "Unsupported SVG element: {}".format(item.__class__.__name__)) + logger.warning( "Unsupported SVG element: {}".format(item.__class__.__name__)) #------------------------------------------------------------------------ @@ -463,7 +464,7 @@ def write( self, cmdline="scripting" ): self._calculate_translation() if self.file_name: - logging.getLogger("unfiltered").info( "Writing module file: {}".format( self.file_name ) ) + unfiltered_logger.info( "Writing module file: {}".format( self.file_name ) ) self.output_file = open( self.file_name, 'w' ) else: self.output_file = io.StringIO() @@ -737,7 +738,7 @@ def __init__( def _parse_output_file( self ): - logging.info( "Parsing module file: {}".format( self.file_name ) ) + logger.info( "Parsing module file: {}".format( self.file_name ) ) module_file = open( self.file_name, 'r' ) lines = module_file.readlines() module_file.close() @@ -808,7 +809,7 @@ def _read_module( self, lines, index ): m = re.match( r'\$MODULE[\s]+([^\s]+)[\s]*', lines[ index ] ) module_name = m.group( 1 ) - logging.info( " Reading module {}".format( module_name ) ) + logger.info( " Reading module {}".format( module_name ) ) index += 1 module_lines = [] @@ -1009,7 +1010,7 @@ def _get_layer_name( self, item_name, name, front ): if allowed in self.keepout_allowed: attrs["allowed"].append(allowed) else: - logging.warning("Invalid allowed option in keepout: {} in {}".format(allowed, arg)) + logger.warning("Invalid allowed option in keepout: {} in {}".format(allowed, arg)) elif re.match(r'^\w+\.Cu', name) and re.match(r'^pad(:(\d+|mask|paste))?', arg, re.I): if arg.lower() == "pad": attrs["copper_pad"] = True @@ -1027,9 +1028,9 @@ def _get_layer_name( self, item_name, name, front ): if not attrs.get("copper_pad"): attrs["copper_pad"] = True else: - logging.warning("Invalid pad option '{}' for layer {}".format(opt, name)) + logger.warning("Invalid pad option '{}' for layer {}".format(opt, name)) else: - logging.warning("Unexpected option: {} for {}".format(arg, item_name[0])) + logger.warning("Unexpected option: {} for {}".format(arg, item_name[0])) if attrs: return name+":"+json.dumps(attrs) return name diff --git a/svg2mod/importer.py b/svg2mod/importer.py index 67be689..7130aeb 100644 --- a/svg2mod/importer.py +++ b/svg2mod/importer.py @@ -20,16 +20,18 @@ Svg2ModExport. ''' -import logging from svg2mod import svg +from svg2mod.coloredlogger import logger, unfiltered_logger +#---------------------------------------------------------------------------- class Svg2ModImport: ''' An importer class to read in target svg, parse it, and keep only layers on interest. ''' + #------------------------------------------------------------------------ def _prune_hidden( self, items = None ): @@ -41,12 +43,14 @@ def _prune_hidden( self, items = None ): if (hasattr(item, "hidden") and item.hidden ) or (hasattr(item, "style") and item.style.get("display") == "none"): if hasattr(item, "name") and item.name: - logging.warning("Ignoring hidden SVG item: {}".format( item.name ) ) + logger.warning("Ignoring hidden SVG item: {}".format( item.name ) ) items.remove(item) if hasattr(item, "items") and item.items: self._prune_hidden( item.items ) + #------------------------------------------------------------------------ + def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", ignore_hidden=False, force_layer=None): self.file_name = file_name @@ -55,10 +59,10 @@ def __init__( self, file_name=None, module_name="svg2mod", module_value="G***", self.ignore_hidden = ignore_hidden if file_name: - logging.getLogger("unfiltered").info( "Parsing SVG..." ) + unfiltered_logger.info( "Parsing SVG..." ) self.svg = svg.parse( file_name ) - logging.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) + logger.info("Document scaling: {} units per pixel".format(self.svg.viewport_scale)) if force_layer: new_layer = svg.Group() new_layer.name = force_layer diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 92e71b7..3bc95b0 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -25,7 +25,6 @@ import json import logging import math -from multiprocessing import parent_process import operator import os import platform @@ -37,6 +36,7 @@ from fontTools.misc import loggingTools from fontTools.pens.svgPathPen import SVGPathPen from fontTools.ttLib import ttFont +from svg2mod.coloredlogger import logger from .geometry import Angle, Bezier, MoveTo, Point, Segment, simplify_segment @@ -98,7 +98,7 @@ def __init__(self, elt=None, parent_styles=None): value = list(re.search(r'(\d+\.?\d*)(\D+)?', value).groups()) self.style[name] = float(value[0]) if value[1] and value[1] not in unit_convert: - logging.warning("Style '{}' has an unexpected unit: {}".format(style, value[1])) + logger.warning("Style '{}' has an unexpected unit: {}".format(style, value[1])) else: self.style[name] = value @@ -139,7 +139,7 @@ def get_transformations(self, elt): op = op.strip() # Keep only numbers arg = [float(x) for x in re.findall(number_re, arg)] - logging.debug('transform: ' + op + ' '+ str(arg)) + logger.debug('transform: ' + op + ' '+ str(arg)) if op == 'matrix': self.matrix *= Matrix(arg) @@ -286,7 +286,7 @@ def parse(self, filename:str): if self.root.get('width') is None or self.root.get('height') is None: width = float(view_box[2]) height = float(view_box[3]) - logging.warning("Unable to find width or height properties. Using viewBox.") + logger.warning("Unable to find width or height properties. Using viewBox.") sx = width / float(view_box[2]) sy = height / float(view_box[3]) @@ -296,7 +296,7 @@ def parse(self, filename:str): top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) if ( self.root.get("width") is None or self.root.get("height") is None ) \ and self.root.get("viewBox") is None: - logging.critical("Fatal Error: Unable to find SVG dimensions. Exiting.") + logger.critical("Fatal Error: Unable to find SVG dimensions. Exiting.") sys.exit(-1) # Parse XML elements hierarchically with groups @@ -359,7 +359,7 @@ def append(self, element): for elt in element: elt_class = svgClass.get(elt.tag, None) if elt_class is None: - logging.debug('No handler for element %s' % elt.tag) + logger.debug('No handler for element %s' % elt.tag) continue # instantiate elt associated class (e.g. : item = Path(elt) item = elt_class(elt, parent_styles=self.style) @@ -572,14 +572,14 @@ def parse(self, pathstr:str): flags = pathlst.pop().strip() large_arc_flag = flags[0] if large_arc_flag not in '01': - logging.error("Arc parsing failure") + logger.error("Arc parsing failure") break if len(flags) > 1: flags = flags[1:].strip() else: flags = pathlst.pop().strip() sweep_flag = flags[0] if sweep_flag not in '01': - logging.error("Arc parsing failure") + logger.error("Arc parsing failure") break if len(flags) > 1: x = flags[1:] @@ -1175,7 +1175,7 @@ def find_font_file(self): if Text.default_font is None: global _font_warning_sent if not _font_warning_sent: - logging.error("Unable to find font because no font was specified.") + logger.error("Unable to find font because no font was specified.") _font_warning_sent = True return None self.font_family = Text.default_font @@ -1191,7 +1191,7 @@ def find_font_file(self): break if font_files is None: # We are unable to find a font and since there is no default font stop building font data - logging.error("Unable to find font(s) \"{}\"{}".format( + logger.error("Unable to find font(s) \"{}\"{}".format( self.font_family, " and no default font specified" if Text.default_font is None else f" or default font \"{Text.default_font}\"" )) @@ -1215,7 +1215,7 @@ def find_font_file(self): tar_font = list(filter(None, [font_files.get(style) for style in search])) if len(tar_font) == 0 and len(font_files.keys()) == 1: tar_font = [font_files[list(font_files.keys())[0]]] - logging.warning("Font \"{}\" does not natively support style \"{}\" using \"{}\" instead".format( + logger.warning("Font \"{}\" does not natively support style \"{}\" using \"{}\" instead".format( target_font, search[0], list(font_files.keys())[0])) elif len(tar_font) == 0 and italic and bold: orig_search = search[0] @@ -1227,7 +1227,7 @@ def find_font_file(self): for style in search: if font_files.get(style) is not None: tar_font = [font_files[style]] - logging.warning("Font \"{}\" does not natively support style \"{}\" using \"{}\" instead".format( + logger.warning("Font \"{}\" does not natively support style \"{}\" using \"{}\" instead".format( target_font, orig_search, style)) break return tar_font[0] @@ -1268,7 +1268,7 @@ def convert_to_path(self, auto_transform=True): pathbuf = "" try: glf = ttf.getGlyphSet()[ttf.getBestCmap()[ord(char)]] except KeyError: - logging.warning('Unsuported character in element "{}"'.format(char)) + logger.warning('Unsuported character in element "{}"'.format(char)) #txt = txt.replace(char, "") continue @@ -1349,7 +1349,7 @@ def load_system_fonts(reload:bool=False) -> List[dict]: Text._system_fonts = {} if len(Text._system_fonts.keys()) < 1: fonts_files = [] - logging.info("Loading system fonts.") + logger.info("Loading system fonts.") for path in Text._os_font_paths[platform.system()]: try: fonts_files.extend([os.path.join(dp, f) for dp, dn, fn in os.walk(os.path.expanduser(path)) for f in fn]) @@ -1367,7 +1367,7 @@ def load_system_fonts(reload:bool=False) -> List[dict]: Text._system_fonts[name][style] = ffile except: pass - logging.debug(f" Found {len(Text._system_fonts.keys())} fonts in system") + logger.debug(f" Found {len(Text._system_fonts.keys())} fonts in system") return Text._system_fonts diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 0b2da6e..3fbba07 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -20,11 +20,10 @@ ''' import copy -import logging from typing import List, Tuple from svg2mod import svg - +from svg2mod.coloredlogger import logger #---------------------------------------------------------------------------- @@ -203,7 +202,7 @@ def __init__( self, points:List): if len( points ) < 3: - logging.warning("Warning: Path segment has only {} points (not a polygon?)".format(len( points ))) + logger.warning("Warning: Path segment has only {} points (not a polygon?)".format(len( points ))) self.bbox = None self.calc_bbox() @@ -279,7 +278,7 @@ def inline( self, segments: List[svg.Point] ) -> List[svg.Point]: if len( segments ) < 1: return self.points - logging.debug( " Inlining {} segments...".format( len( segments ) ) ) + logger.debug( " Inlining {} segments...".format( len( segments ) ) ) segments.sort(reverse=True, key=lambda h: h.bbox[1].y) From faad6d9878a1342acb794e31cb63826498ab7b91 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 2 Feb 2022 14:43:14 -0700 Subject: [PATCH 129/151] Fix empty writing detection --- svg2mod/cli.py | 6 +++--- svg2mod/exporter.py | 13 ++++++++----- svg2mod/importer.py | 2 +- svg2mod/svg/svg.py | 7 +++---- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/svg2mod/cli.py b/svg2mod/cli.py index 8c2b90f..0904f44 100644 --- a/svg2mod/cli.py +++ b/svg2mod/cli.py @@ -49,10 +49,10 @@ def main(): # Setup root logger to use terminal colored outputs as well as stdout and stderr coloredlogger.split_logger(logger) - if args.verbose_print: - logger.setLevel(logging.INFO) - elif args.debug_print: + if args.debug_print: logger.setLevel(logging.DEBUG) + elif args.verbose_print: + logger.setLevel(logging.INFO) else: logger.setLevel(logging.WARNING) diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index 30b9fca..b3a60e0 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -213,6 +213,8 @@ def _calculate_translation( self ): def _prune( self, items = None ): '''Find and keep only the layers of interest.''' + empty_group_exception = items is None + if items is None: self.layers = {} @@ -254,11 +256,12 @@ def _prune( self, items = None ): logger.debug( " Detailed names: [{}]".format( ", ".join(kept_layers[kept]) ) ) # There are no elements to write so don't write - for name in self.layers: - if self.layers[name]: - break - else: - raise Exception("Not writing empty file. No valid items found.") + if empty_group_exception: + for name in self.layers: + if self.layers[name]: + break + else: + raise Exception("Not writing empty file. No valid items found.") #------------------------------------------------------------------------ diff --git a/svg2mod/importer.py b/svg2mod/importer.py index 7130aeb..d641dca 100644 --- a/svg2mod/importer.py +++ b/svg2mod/importer.py @@ -41,7 +41,7 @@ def _prune_hidden( self, items = None ): for item in items[:]: - if (hasattr(item, "hidden") and item.hidden ) or (hasattr(item, "style") and item.style.get("display") == "none"): + if hasattr(item, "hidden") and item.hidden: if hasattr(item, "name") and item.name: logger.warning("Ignoring hidden SVG item: {}".format( item.name ) ) items.remove(item) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 3bc95b0..19a5d6f 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -105,6 +105,9 @@ def __init__(self, elt=None, parent_styles=None): # Parse transform attribute to update self.matrix self.get_transformations(elt) + if self.style.get("display") == "none": + self.hidden = True + def bbox(self): '''Bounding box of all points''' bboxes = [x.bbox() for x in self.items] @@ -329,16 +332,12 @@ def __init__(self, elt=None, *args, **kwargs): Transformable.__init__(self, elt, *args, **kwargs) self.name = "" - self.hidden = False if elt is not None: for ident, value in elt.attrib.items(): ident = self.parse_name( ident ) if ident[ "name" ] == "label": self.name = value - if ident[ "name" ] == "style": - if re.search( r"display\s*:\s*none", value ): - self.hidden = True @staticmethod def parse_name( tag ): From d7df1cca8608e5b1383c3436b07e23e874a6bcdc Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Thu, 3 Feb 2022 14:35:05 -0700 Subject: [PATCH 130/151] Improve output for paths with < 3 points --- svg2mod/exporter.py | 65 +++++++++++++++++++-------------------------- svg2mod/svg2mod.py | 5 +--- 2 files changed, 29 insertions(+), 41 deletions(-) diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index b3a60e0..0fd1cfa 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -20,6 +20,7 @@ ''' +import copy import datetime import io import json @@ -76,9 +77,6 @@ def _write_module_footer( self, front ):pass @abstractmethod def _write_polygon_header( self, points, layer ):pass - @abstractmethod - def _write_polygon( self, points, layer, fill, stroke, stroke_width ):pass - @abstractmethod def _write_polygon_footer( self, layer, stroke_width, fill=True):pass @@ -395,6 +393,33 @@ def _write_module( self, front ): #------------------------------------------------------------------------ + def _write_polygon( self, points, layer, fill, stroke, stroke_width ): + + if fill and len(points) > 2: + self._write_polygon_filled( + points, layer, stroke_width + ) + return + + # Polygons with a fill and stroke are drawn with the filled polygon above + if stroke: + if len(points) == 1: + points.append(copy.copy(points[0])) + + self._write_polygon_outline( + points, layer, stroke_width + ) + return + + if len(points) < 3: + logger.debug(" Not writing non-polygon with no stroke.") + else: + logger.debug(" Polygon has no stroke or fill. Skipping.") + + + #------------------------------------------------------------------------ + + def _write_polygon_filled( self, points, layer, stroke_width = 0.0 ): self._write_polygon_header( points, layer ) @@ -636,22 +661,6 @@ def _write_modules( self ): self.output_file.write( "$EndLIBRARY" ) - #------------------------------------------------------------------------ - - def _write_polygon( self, points, layer, fill, stroke, stroke_width ): - - if fill: - self._write_polygon_filled( - points, layer - ) - - if stroke: - - self._write_polygon_outline( - points, layer, stroke_width - ) - - #------------------------------------------------------------------------ def _write_polygon_footer( self, layer, stroke_width, fill=True ): @@ -1121,24 +1130,6 @@ def _write_polygon_filled( self, points, layer, stroke_width = 0): #------------------------------------------------------------------------ - def _write_polygon( self, points, layer, fill, stroke, stroke_width ): - - if fill: - self._write_polygon_filled( - points, layer, stroke_width - ) - - # Polygons with a fill and stroke are drawn with the filled polygon - # above: - if stroke and not fill: - - self._write_polygon_outline( - points, layer, stroke_width - ) - - - #------------------------------------------------------------------------ - def _write_polygon_footer( self, layer, stroke_width, fill=True ): #Format option #2 is expected, but only used in Svg2ModExportLatest diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 3fbba07..0ac48b0 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -194,16 +194,13 @@ class PolygonSegment: def __init__( self, points:List): - self.points = [points.pop(0)] + self.points = [points[0]] for point in points: if self.points[-1] != point: self.points.append(point) - if len( points ) < 3: - logger.warning("Warning: Path segment has only {} points (not a polygon?)".format(len( points ))) - self.bbox = None self.calc_bbox() From b620af0f0e7a9e588f5cca0a690873d656d9cda8 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Thu, 3 Feb 2022 21:15:30 -0700 Subject: [PATCH 131/151] Revert downloads badge to pypistats.org --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91087a0..25098cf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version&style=for-the-badge)](https://pypi.org/project/svg2mod/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) -[![PyPI - Downloads](https://img.shields.io/badge/dynamic/xml?style=for-the-badge&color=green&label=downloads&query=%2F%2F%2A%5Blocal-name%28%29%20%3D%20%27text%27%5D%5Blast%28%29%5D&suffix=%2Fmonth&url=https%3A%2F%2Fstatic.pepy.tech%2Fbadge%2Fsvg2mod%2Fmonth)](https://pypi.org/project/svg2mod/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/sodium-hydrogen?logo=github&style=for-the-badge)](https://github.com/sponsors/Sodium-Hydrogen) From e20379ca58f8bf0eb5f8f91b25cb06145f2a6d0e Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 4 Feb 2022 11:40:43 -0700 Subject: [PATCH 132/151] Add variable hatching for keepout zones --- svg2mod/exporter.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index 0fd1cfa..706bd4a 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -1015,7 +1015,9 @@ def _get_layer_name( self, item_name, name, front ): if len(item_name) == 2 and item_name[1]: for arg in item_name[1].split(';'): arg = arg.strip(' ,:') + # This is used in Svg2ModExportLatest as it is a breaking change + # Keepout allowed items if name == "Keepout" and re.match(r'^allowed:\w+', arg, re.I): attrs["allowed"] = [] for allowed in arg.lower().split(":", 1)[1].split(','): @@ -1023,6 +1025,11 @@ def _get_layer_name( self, item_name, name, front ): attrs["allowed"].append(allowed) else: logger.warning("Invalid allowed option in keepout: {} in {}".format(allowed, arg)) + # Zone hatch patterns + elif name == "Keepout" and re.match(r'^hatch:(none|edge|full)$', arg, re.I): + attrs["hatch"] = arg.split(":", 1)[1] + + #Copper pad attributes elif re.match(r'^\w+\.Cu', name) and re.match(r'^pad(:(\d+|mask|paste))?', arg, re.I): if arg.lower() == "pad": attrs["copper_pad"] = True @@ -1175,21 +1182,22 @@ def _write_polygon_header( self, points, layer, stroke_width): layers = options["layers"][:] + ['In{}'.format(i) for i in range(1,31)] - self.output_file.write( '''\n (zone (net 0) (net_name "") (layers "{0}.Cu") (hatch edge {1:.6f}) + self.output_file.write( '''\n (zone (net 0) (net_name "") (layers "{0}.Cu") (hatch {1} {2:.6f}) (connect_pads (clearance 0)) - (min_thickness {2:.6f}) - (keepout ({3}allowed)) - (fill (thermal_gap {1:.6f}) (thermal_bridge_width {1:.6f})) + (min_thickness {3:.6f}) + (keepout ({4}allowed)) + (fill (thermal_gap {2:.6f}) (thermal_bridge_width {2:.6f})) (polygon (pts\n'''.format( '.Cu" "'.join(layers), #0 - stroke_width, #1 - stroke_width/2, #2 + options["hatch"] if options.get("hatch") else "full", #1 + stroke_width, #2 + stroke_width/2, #3 "allowed) (".join( [i+" "+( "not_" if not options.get("allowed") or i not in options["allowed"] else "" ) for i in self.keepout_allowed] - ), #3 + ), #4 ) ) self._special_footer = " )\n )\n )" From adab0092664121769896bba60d3d52e3f1b809d5 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Fri, 1 Apr 2022 09:24:06 -0600 Subject: [PATCH 133/151] Add markdown linting --- .github/workflows/markdown-lint.yml | 27 +++++++++++++++ .linter/.markdownlint.yaml | 4 +++ .linter/cspell.json | 9 +++++ .linter/custom_dict | 7 +++- README.md | 53 ++++++++++++++++------------- 5 files changed, 76 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/markdown-lint.yml create mode 100644 .linter/.markdownlint.yaml create mode 100644 .linter/cspell.json diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml new file mode 100644 index 0000000..1fb3c61 --- /dev/null +++ b/.github/workflows/markdown-lint.yml @@ -0,0 +1,27 @@ +name: Documentation Linting + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16.x + - name: Installing dependencies + run: | + npm install -g markdownlint-cli2 + npm install -g cspell + - name: Lint + run: markdownlint-cli2-config ".linter/.markdownlint.yaml" "*.md" + - name: Spellcheck + run: cspell -c .linter/cspell.json "*.md" --show-suggestions + \ No newline at end of file diff --git a/.linter/.markdownlint.yaml b/.linter/.markdownlint.yaml new file mode 100644 index 0000000..0d39f14 --- /dev/null +++ b/.linter/.markdownlint.yaml @@ -0,0 +1,4 @@ +default: true + +MD013: + line_length: 100 \ No newline at end of file diff --git a/.linter/cspell.json b/.linter/cspell.json new file mode 100644 index 0000000..9158496 --- /dev/null +++ b/.linter/cspell.json @@ -0,0 +1,9 @@ +{ + "dictionaries": ["custom_dict"], + "dictionaryDefinitions": [ + { + "name": "custom_dict", + "path": "./custom_dict" + } + ] +} \ No newline at end of file diff --git a/.linter/custom_dict b/.linter/custom_dict index dab75d0..8c2377f 100644 --- a/.linter/custom_dict +++ b/.linter/custom_dict @@ -44,4 +44,9 @@ Regex thru Faux getLogger -keepout \ No newline at end of file +keepout +fonttools +adhes +dwgs +cmts +copperpour \ No newline at end of file diff --git a/README.md b/README.md index 25098cf..67d4d1f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # svg2mod + [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/svg2mod/svg2mod/Python%20lint%20and%20test?logo=github&style=for-the-badge)](https://github.com/svg2mod/svg2mod/actions/workflows/python-package.yml) [![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod?style=for-the-badge)](https://github.com/svg2mod/svg2mod/commits/main) @@ -6,13 +7,14 @@ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) - [![GitHub Sponsors](https://img.shields.io/github/sponsors/sodium-hydrogen?logo=github&style=for-the-badge)](https://github.com/sponsors/Sodium-Hydrogen) [![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=purple&style=for-the-badge)](https://pypi.org/project/svg2mod/) This is a program / library to convert SVG drawings to KiCad footprint module files. -It includes a modified version of [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. +It includes a modified version of [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 @@ -28,7 +30,7 @@ It includes a modified version of [cjlano's python SVG parser and drawing module We'd love to see the amazing projects that use svg2mod. If you have a project you are proud of please post about it on our -[github discussions board ](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) +[github discussions board](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) [![GitHub Discussions](https://img.shields.io/github/discussions/svg2mod/svg2mod?logo=github&style=for-the-badge)](https://github.com/svg2mod/svg2mod/discussions/categories/show-and-tell) @@ -78,18 +80,26 @@ optional arguments: ## SVG Files -svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. This is so it can associate inkscape layers with kicad layers +svg2mod expects images saved in the uncompressed Inkscape SVG (i.e., not "plain SVG") format. This +is so it can associate inkscape layers with kicad layers + +* Drawings should be to scale (1 mm in Inkscape will be 1 mm in KiCad). Use the --factor option to +resize the resulting module(s) up or down from there. -* 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. * Most elements are fully supported. * A path may have an outline and a fill. (Colors will be ignored.) * A path may have holes, defined by interior segments within the path (see included examples). * 100% Transparent fills and strokes with be ignored. * Text Elements are partially supported -* Groups may be used. Styles applied to groups (e.g., stroke-width) are applied to contained drawing elements. -* Layers must be named to match the target in kicad. The supported layers are listed below. They will be ignored otherwise. +* Groups may be used. Styles applied to groups (e.g., stroke-width) are applied to contained drawing + elements. + +* Layers must be named to match the target in kicad. The supported layers are listed below. They will + be ignored otherwise. + * __If there is an issue parsing an inkscape object or stroke convert it to a path.__ - * __Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these elements into paths that will work.__ + * __Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these__ + __elements into paths that will work.__ ### Layers @@ -120,19 +130,20 @@ This supports the layers listed below. They are the same in inkscape and kicad: | Drill.Mech [^1] [^2] | -- | Yes | | *.Keepout [^1] [^4] | -- | Yes [^3] | -Note: If you have a layer `F.Cu`, all of its sub-layers will be treated as `F.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. ### Layer Options Some layers can have options when saving to the newer 'pretty' format. -The options are seperated from the layer name by `:`. Ex `F.Cu:...` +The options are separated from the layer name by `:`. Ex `F.Cu:...` -Some options can have arguments which are also seperated from +Some options can have arguments which are also separated from the option key by `:`. If an option has more than one argument they -are seperated by a comma. Ex: `F.Cu:Pad:1,mask`. +are separated by a comma. Ex: `F.Cu:Pad:1,mask`. -If a layer has more than one option they will be seperated by `;` +If a layer has more than one option they will be separated by `;` Ex: `F.Cu:pad;...` Supported Arguments: @@ -143,27 +154,23 @@ Supported Arguments: The pad option can be used solo (`F.Cu:Pad`) or it can also have it's own arguments. The arguments are: - * Number + * Number If it is set it will specify the number of the pad. Ex: `Pad:1` - * Paste _(Not avalable for `Drill.Cu`)_ - * Mask _(Not avalable for `Drill.Cu`)_ - + * Paste _(Not available for `Drill.Cu`)_ + * Mask _(Not available for `Drill.Cu`)_ * Allowed Keepout areas will prevent anything from being placed inside them. - To allow some things to be placed inside the keepout zone a comma - seperated list of any of the following options can be used: + To allow some things to be placed inside the keepout zone a comma + separated list of any of the following options can be used: `tracks`,`vias`,`pads`,`copperpour`,`footprints` - - - [^1]: These layers can have arguments when svg2mod is in pretty mode [^2]: Drills can only be svg circle objects. The stroke width in `Drill.Cu` is the pad size and the fill is the drill size. [^3]: Only works in Kicad versions >= v6 (`--format latest`). -[^4]: The * can be { *, F, B, I } or any combination like FB or BI. These options are for Front, Back, and Internal. +[^4]: The \* can be { \*, F, B, I } or any combination like FB or BI. These options are for Front, Back, and Internal. From 39ac4b0425f8b615e3237229e96837cbe4d8b0a8 Mon Sep 17 00:00:00 2001 From: Rutuparn Pawar Date: Sun, 10 Jul 2022 13:34:36 +0530 Subject: [PATCH 134/151] README: Information regarding web application added --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 25098cf..496aa2b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,11 @@ It includes a modified version of [cjlano's python SVG parser and drawing module ```pip install svg2mod``` +#### Do not want to install the application? +Use the web application instead: https://inputblackboxoutput-streamlit-svg2mod-app-fszqih.streamlitapp.com/ + +Please request features and report bugs related to this web application at this [GitHub repository](https://github.com/InputBlackBoxOutput/streamlit-svg2mod) + ## Showcase We'd love to see the amazing projects that use svg2mod. From 39aae2b5856ab6a59a947a0471450739cc17022e Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 11 Jul 2022 09:33:17 -0600 Subject: [PATCH 135/151] Improve UX with better error msg and cli --- README.md | 27 +++++++++++++++++---------- svg2mod/cli.py | 15 +++++++++++++-- svg2mod/exporter.py | 1 + 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 67d4d1f..bb7cf1b 100644 --- a/README.md +++ b/README.md @@ -36,21 +36,26 @@ If you have a project you are proud of please post about it on our ## Example -```svg2mod -i input.svg``` +```svg2mod input.svg``` ## Usage ```text -usage: svg2mod [-h] [-i FILENAME] [-o FILENAME] [-c] [-P] [-v] [--debug] [-x] [--force LAYER] -[-d DPI] [-f FACTOR] [-p PRECISION] [--format FORMAT] [--name NAME] [--units UNITS] - [--value VALUE] [-F DEFAULT_FONT] [-l] +usage: svg2mod [-h] [-i FILENAME] [-o FILENAME] [-c] [-P] [-v] [--debug] [-x] + [--force LAYER] [-d DPI] [-f FACTOR] [-p PRECISION] + [--format FORMAT] [--name NAME] [--units UNITS] [--value VALUE] + [-F DEFAULT_FONT] [-l] + [IN_FILENAME] Convert Inkscape SVG drawings to KiCad footprint modules. +positional arguments: + IN_FILENAME Name of the SVG file + optional arguments: -h, --help show this help message and exit -i FILENAME, --input-file FILENAME - Name of the SVG file + Name of the SVG file, but specified with a flag. -o FILENAME, --output-file FILENAME Name of the module file -c, --center Center the module to the center of the bounding box @@ -64,17 +69,19 @@ optional arguments: -f FACTOR, --factor FACTOR Scale paths by this factor -p PRECISION, --precision PRECISION - Smoothness for approximating curves with line segments. Input is the - approximate length for each line segment in SVG pixels (float) - --format FORMAT Output module file format (legacy|pretty|latest). 'latest' introduces - features used in kicad >= 6 + Smoothness for approximating curves with line + segments. Input is the approximate length for each + line segment in SVG pixels (float) + --format FORMAT Output module file format (legacy|pretty|latest). + 'latest' introduces features used in kicad >= 6 --name NAME, --module-name NAME Base name of the module --units UNITS Output units, if output format is legacy (decimal|mm) --value VALUE, --module-value VALUE Value of the module -F DEFAULT_FONT, --default-font DEFAULT_FONT - Default font to use if the target font in a text element cannot be found + Default font to use if the target font in a text + element cannot be found -l, --list-fonts List all fonts that can be found in common locations ``` diff --git a/svg2mod/cli.py b/svg2mod/cli.py index 0904f44..1ace1c8 100644 --- a/svg2mod/cli.py +++ b/svg2mod/cli.py @@ -56,6 +56,9 @@ def main(): else: logger.setLevel(logging.WARNING) + if args.input_file_name_flag and not args.input_file_name: + args.input_file_name = args.input_file_name_flag + if args.list_fonts: fonts = svg.Text.load_system_fonts() unfiltered_logger.info("Font Name: list of supported styles.") @@ -174,13 +177,21 @@ def get_arguments(): mux = parser.add_mutually_exclusive_group(required=True) mux.add_argument( - '-i', '--input-file', + nargs="?", type = str, dest = 'input_file_name', - metavar = 'FILENAME', + metavar = 'IN_FILENAME', help = "Name of the SVG file", ) + mux.add_argument( + '-i', '--input-file', + type = str, + dest = 'input_file_name_flag', + metavar = 'FILENAME', + help = "Name of the SVG file, but specified with a flag.", + ) + parser.add_argument( '-o', '--output-file', type = str, diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index 706bd4a..b3a84e8 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -259,6 +259,7 @@ def _prune( self, items = None ): if self.layers[name]: break else: + logger.warning("No valid items found. Maybe try --force Layer.Name") raise Exception("Not writing empty file. No valid items found.") #------------------------------------------------------------------------ From 87f890d28799b0ecc7a545d263a693d0ecda7fcd Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Mon, 11 Jul 2022 10:00:22 -0600 Subject: [PATCH 136/151] Add new hatch arguments to readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index bb7cf1b..98158ec 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,12 @@ Supported Arguments: separated list of any of the following options can be used: `tracks`,`vias`,`pads`,`copperpour`,`footprints` +* Hatch + + Keepout areas have different hatching styles. This allows customization + of the appearance of hatching when converting from an svg, Ex: `F.Keepout:Hatch:edge`. + All available hatch options are `none`, `edge`, `full`. + [^1]: These layers can have arguments when svg2mod is in pretty mode [^2]: Drills can only be svg circle objects. The stroke width in `Drill.Cu` is the pad size and the fill is the drill size. From def906e5705e092db4db41d1d49f5d101237ef7b Mon Sep 17 00:00:00 2001 From: Rutuparn Pawar Date: Mon, 11 Jul 2022 22:31:58 +0530 Subject: [PATCH 137/151] README: Web applications list --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 496aa2b..8689659 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ It includes a modified version of [cjlano's python SVG parser and drawing module ```pip install svg2mod``` -#### Do not want to install the application? -Use the web application instead: https://inputblackboxoutput-streamlit-svg2mod-app-fszqih.streamlitapp.com/ - -Please request features and report bugs related to this web application at this [GitHub repository](https://github.com/InputBlackBoxOutput/streamlit-svg2mod) +### Do not want to install the application? +Use the web application instead: +- https://inputblackboxoutput-streamlit-svg2mod-app-fszqih.streamlitapp.com/ +- https://svg2mod.com/ ## Showcase From 5cbf86f18f11c0d9667d033080619aca8e1edc52 Mon Sep 17 00:00:00 2001 From: Rutuparn Pawar Date: Fri, 15 Jul 2022 08:48:16 +0530 Subject: [PATCH 138/151] README: Fix linting errors --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8689659..c3b0269 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,10 @@ It includes a modified version of [cjlano's python SVG parser and drawing module ```pip install svg2mod``` ### Do not want to install the application? + Use the web application instead: -- https://inputblackboxoutput-streamlit-svg2mod-app-fszqih.streamlitapp.com/ -- https://svg2mod.com/ +* [svg2mod.streamlitapp.com](https://inputblackboxoutput-streamlit-svg2mod-app-fszqih.streamlitapp.com/) +* [svg2mod.com](https://svg2mod.com/) ## Showcase From 3e12a8989fbea0aa8f381a69464d12808373c6c3 Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Tue, 19 Jul 2022 11:00:52 -0600 Subject: [PATCH 139/151] Fix failed checks for readme linting --- .linter/custom_dict | 3 ++- README.md | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.linter/custom_dict b/.linter/custom_dict index 8c2377f..94cae47 100644 --- a/.linter/custom_dict +++ b/.linter/custom_dict @@ -49,4 +49,5 @@ fonttools adhes dwgs cmts -copperpour \ No newline at end of file +copperpour +streamlitapp diff --git a/README.md b/README.md index 1f28b7a..4738d2f 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,10 @@ output in KiCad's legacy or s-expression (i.e., pretty) formats. ```pip install svg2mod``` -### Do not want to install the application? +### Don't want to install the application? + +You can use a 3rd party web application instead: -Use the web application instead: * [svg2mod.streamlitapp.com](https://inputblackboxoutput-streamlit-svg2mod-app-fszqih.streamlitapp.com/) * [svg2mod.com](https://svg2mod.com/) From e912765d230cbe811068b502ab846c26a5bf4d70 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 31 Oct 2022 21:52:32 -0600 Subject: [PATCH 140/151] Fix bugs and work on non-inkscape support. Fix bug #51 by fixing inlining segment selection. Add basic support for polygons. Allow layer names to also be item names and if an item doesn't have a layer name set it from the id. --- svg2mod/exporter.py | 21 +++++++++-- svg2mod/svg/svg.py | 89 ++++++++++++++++++++++++++++++++++++--------- svg2mod/svg2mod.py | 31 ++++++++++++++-- 3 files changed, 118 insertions(+), 23 deletions(-) diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index b3a84e8..aa7c165 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -227,8 +227,11 @@ def _prune( self, items = None ): for item in items: - if not isinstance( item, svg.Group ): + # if not isinstance( item, svg.Group ): + # continue + if not hasattr(item, 'name'): continue + i_name = item.name.split(":", 1) for name in self.layers.keys(): @@ -243,6 +246,14 @@ def _prune( self, items = None ): else: kept_layers[i_name[0]] = [item.name] + # Item isn't a group so make it one + if not isinstance(item, svg.Group): + grp = svg.Group() + grp.name = item.name + grp.items.append( item ) + item = grp + + # save valid groups self.imported.svg.items.append( item ) self.layers[name].append((i_name, item)) break @@ -273,12 +284,12 @@ def _write_items( self, items, layer, flip = False ): continue if re.match(r"^Drill\.\w+", str(layer)): - if isinstance(item, svg.Circle): + if isinstance(item, (svg.Circle, svg.Ellipse)): self._write_thru_hole(item, layer) else: logger.warning( "Non Circle SVG element in drill layer: {}".format(item.__class__.__name__)) - elif isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text)): + elif isinstance( item, (svg.Path, svg.Ellipse, svg.Rect, svg.Text, svg.Polygon)): segments = [ PolygonSegment( segment ) @@ -1308,6 +1319,10 @@ def _write_polygon_segment( self, p, q, layer, stroke_width ): def _write_thru_hole( self, circle, layer ): + if not isinstance(circle, svg.Circle): + logger.info("Found an ellipse in Drill layer. Using an average of rx and ry.") + circle.rx = (circle.rx + circle.ry ) / 2 + l_name = layer options = {} try: diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 19a5d6f..7e86228 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -75,6 +75,7 @@ def __init__(self, elt=None, parent_styles=None): # a 'Transformable' is represented as a list of Transformable items self.items = [] self.id = hex(id(self)) + self.name = "" # Unit transformation matrix on init self.matrix = Matrix() self.scalex = 1 @@ -85,6 +86,19 @@ def __init__(self, elt=None, parent_styles=None): if elt is not None: self.id = elt.get('id', self.id) + # get inkscape:label as self.name + for ident, value in elt.attrib.items(): + + ident = self.parse_name( ident ) + if ident[ "name" ] == "label": + self.name = value + break + # self.name isn't set so try setting name to id + if self.name == '': + self.name == self.id + + + # parse styles and save as dictionary. if elt.get('style'): for style in elt.get('style').split(";"): @@ -108,6 +122,15 @@ def __init__(self, elt=None, parent_styles=None): if self.style.get("display") == "none": self.hidden = True + @staticmethod + def parse_name( tag ): + '''Read and return name from xml data''' + m = re.match( r'({(.+)})?(.+)', tag ) + return { + 'namespace' : m.group( 2 ), + 'name' : m.group( 3 ), + } + def bbox(self): '''Bounding box of all points''' bboxes = [x.bbox() for x in self.items] @@ -331,23 +354,6 @@ class Group(Transformable): def __init__(self, elt=None, *args, **kwargs): Transformable.__init__(self, elt, *args, **kwargs) - self.name = "" - if elt is not None: - for ident, value in elt.attrib.items(): - - ident = self.parse_name( ident ) - if ident[ "name" ] == "label": - self.name = value - - @staticmethod - def parse_name( tag ): - '''Read and return name from xml data''' - m = re.match( r'({(.+)})?(.+)', tag ) - return { - 'namespace' : m.group( 2 ), - 'name' : m.group( 3 ), - } - def append(self, element): '''Convert and append xml element(s) to items list element is expected to be iterable. @@ -624,6 +630,55 @@ def simplify(self, precision:float) -> List[Segment]: return ret +class Polygon(Transformable): + '''SVG tag handler + A polygon has a space separated list of points in format x,y. + + Additionally, polygons can have a style of `file-rule: evenodd;` + which allows intersections to cause the polygon to have holes. + ''' + + # class Polygon handles the tag + tag = 'polygon' + + def __init__(self, elt, *args, **kwargs): + self.path_len = -1 + Transformable.__init__(self, elt, *args, **kwargs) + if elt is not None: + if elt.get('pathLength'): + self.path_len = int(elt.get('pathLength')) + self.parse(elt.get('points')) + logger.warning("Polygons are partially supported and may not give expected results.") + + def parse(self, point_str): + start_pt = None + current_pt = None + + for pair in point_str.split(' '): + if pair: + start_pt = current_pt + current_pt = Point(*pair.split(',')) + + if start_pt and current_pt: + self.items.append(Segment(start_pt, current_pt)) + + def __str__(self): + return ' '.join(str(x) for x in self.items) + + def __repr__(self) -> str: + return '' + + def segments(self, precision=0) -> List[Segment]: + '''Simplify segments if evenodd then cutout holes''' + seg = [x.segments(precision) for x in self.items] + + return [list(itertools.chain.from_iterable(seg))] + + def simplify(self, precision:float) -> List[Segment]: + '''Simplify segments if evenodd then cutout holes: + Remove any point which are ~aligned''' + return [simplify_segment(self.segments()[0], precision)] + class Ellipse(Transformable): '''SVG tag handler An ellipse is created by the center point (center) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 0ac48b0..1c98994 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -238,7 +238,33 @@ def _find_insertion_point( self, hole: 'PolygonSegment', holes: list, other_inse if best[2] != best[1][0] and best[2] != best[1][1]: p = best[0].points.index(best[1][0]) + p_cnt = best[0].points.count(best[1][0]) + q = best[0].points.index(best[1][1]) + q_cnt = best[0].points.count(best[1][1]) + + best_len = len(best[0].points) + + tried = [[p],[q]] + # The same point can be present multiple times without being part of the + # desired segment. The points are also not next to each other. + while ( + (p_cnt > 1 or q_cnt > 1) and + (p + 1)%best_len != q and + (p - 1)%best_len != q + ): + if len(tried[0]) < p_cnt: + p = best[0].points.index(best[1][0], p+1) + tried[0].append(p) + elif len(tried[1]) < q_cnt: + p = tried[0][0] + tried[0] = [p] + q = best[0].points.index(best[1][1], q+1) + tried[1].append(q) + else: + logger.error("Unable to find segment for inlining.") + break + ip = p if p < q else q best[0]._set_points(best[0].points[:ip+1] + [best[2]] + best[0].points[ip+1:]) @@ -285,9 +311,8 @@ def inline( self, segments: List[svg.Point] ) -> List[svg.Point]: # Find the insertion point for each hole: for hole in segments: - insertion = self._find_insertion_point( - hole, all_segments, insertions - ) + insertion = self._find_insertion_point( hole, all_segments, insertions) + if insertion is not None: insertions.append( insertion ) From 452c9abe766ae47d35884f9e7b1021f3fe02711e Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 1 Nov 2022 09:11:41 -0600 Subject: [PATCH 141/151] Cleanup code and fix basic polygon parsing --- .linter/custom_dict | 1 + svg2mod/exporter.py | 2 -- svg2mod/svg/svg.py | 16 ++++++++-------- svg2mod/svg2mod.py | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.linter/custom_dict b/.linter/custom_dict index 94cae47..be06750 100644 --- a/.linter/custom_dict +++ b/.linter/custom_dict @@ -51,3 +51,4 @@ dwgs cmts copperpour streamlitapp +evenodd \ No newline at end of file diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index aa7c165..43c9bb3 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -227,8 +227,6 @@ def _prune( self, items = None ): for item in items: - # if not isinstance( item, svg.Group ): - # continue if not hasattr(item, 'name'): continue diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 7e86228..d1e795e 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -97,8 +97,6 @@ def __init__(self, elt=None, parent_styles=None): if self.name == '': self.name == self.id - - # parse styles and save as dictionary. if elt.get('style'): for style in elt.get('style').split(";"): @@ -651,16 +649,18 @@ def __init__(self, elt, *args, **kwargs): logger.warning("Polygons are partially supported and may not give expected results.") def parse(self, point_str): + '''Split the points from point_str and create a list of segments''' start_pt = None current_pt = None - for pair in point_str.split(' '): - if pair: - start_pt = current_pt - current_pt = Point(*pair.split(',')) + points = re.findall(number_re, point_str) + points.reverse() + while points: + start_pt = current_pt + current_pt = Point(points.pop(), points.pop()) - if start_pt and current_pt: - self.items.append(Segment(start_pt, current_pt)) + if start_pt and current_pt: + self.items.append(Segment(start_pt, current_pt)) def __str__(self): return ' '.join(str(x) for x in self.items) diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 1c98994..41087b8 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -249,7 +249,7 @@ def _find_insertion_point( self, hole: 'PolygonSegment', holes: list, other_inse # The same point can be present multiple times without being part of the # desired segment. The points are also not next to each other. while ( - (p_cnt > 1 or q_cnt > 1) and + (p_cnt > 1 or q_cnt > 1) and (p + 1)%best_len != q and (p - 1)%best_len != q ): From 901c540636e56a7177c5723dd8cc52076c12da40 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 1 Nov 2022 15:24:00 -0600 Subject: [PATCH 142/151] Fix ellipse and arc bounding box calculations. --- .gitignore | 5 ++++- README.md | 5 +++-- svg2mod/svg/svg.py | 30 +++++++++++++++++++----------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 1bc0947..d8113fa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ __pycache__ .pytest_cache Pipfile Pipfile.lock -.vscode \ No newline at end of file +.vscode +*.zip +*.cmd +*.sh \ No newline at end of file diff --git a/README.md b/README.md index 4738d2f..d92292f 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,9 @@ resize the resulting module(s) up or down from there. * Groups may be used. Styles applied to groups (e.g., stroke-width) are applied to contained drawing elements. -* Layers must be named to match the target in kicad. The supported layers are listed below. They will - be ignored otherwise. +* Layers or items must be named to match the target in kicad. The supported layers are listed below. + They will be ignored otherwise. + * These are pulled from `inkscape:label` but will pull from `id` if the label isn't set. * __If there is an issue parsing an inkscape object or stroke convert it to a path.__ * __Use Inkscape's "Path->Object To Path" and "Path->Stroke To Path" menu options to convert these__ diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index d1e795e..cdc168e 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -308,15 +308,15 @@ def parse(self, filename:str): # If the document somehow doesn't have dimensions get if from viewBox if self.root.get('width') is None or self.root.get('height') is None: - width = float(view_box[2]) - height = float(view_box[3]) - logger.warning("Unable to find width or height properties. Using viewBox.") + width = float(view_box[2]) - float(view_box[0]) + height = float(view_box[3]) - float(view_box[1]) + logger.debug("Unable to find width or height properties. Using viewBox.") - sx = width / float(view_box[2]) - sy = height / float(view_box[3]) + sx = width / (float(view_box[2]) - float(view_box[0])) + sy = height / (float(view_box[3]) - float(view_box[1])) tx = -float(view_box[0]) ty = -float(view_box[1]) - self.viewport_scale = round(float(view_box[2])/width, 6) + self.viewport_scale = round((float(view_box[2]) - float(view_box[0]))/width, 6) top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) if ( self.root.get("width") is None or self.root.get("height") is None ) \ and self.root.get("viewBox") is None: @@ -714,11 +714,19 @@ def __repr__(self): return '' def bbox(self) -> Tuple[Point, Point]: - '''Bounding box''' - #TODO change bounding box dependent on rotation - pmin = self.center - Point(self.rx, self.ry) - pmax = self.center + Point(self.rx, self.ry) - return (pmin, pmax) + '''Approximate the bounding box for the given ellipse by + decomposing the ellipse into a small number of segments. + + While there may be better ways of computing this + it is much easier to compute the bounding box of segments. + ''' + points = self.segments((self.rx+self.ry) / 8) + xmin = min([p.x for p in points]) + xmax = max([p.x for p in points]) + ymin = min([p.y for p in points]) + ymax = max([p.y for p in points]) + + return (Point(xmin,ymin),Point(xmax,ymax)) def transform(self, matrix=None): '''Apply the provided matrix. Default (None) From 54db14664368ebcf355ab8b516232563d78f95a1 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 1 Nov 2022 15:44:03 -0600 Subject: [PATCH 143/151] Fix bug with circle/ellipse bounding box calc --- svg2mod/svg/svg.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index cdc168e..6a0c6bc 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -720,7 +720,12 @@ def bbox(self) -> Tuple[Point, Point]: While there may be better ways of computing this it is much easier to compute the bounding box of segments. ''' + if self.arc: + return Transformable.bbox(self) + points = self.segments((self.rx+self.ry) / 8) + points = list(itertools.chain.from_iterable(points)) + xmin = min([p.x for p in points]) xmax = max([p.x for p in points]) ymin = min([p.y for p in points]) From 8c1ba4e3c456ad9ccb44f8021cd4f29b26d4f49a Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 1 Nov 2022 16:14:53 -0600 Subject: [PATCH 144/151] Clean up spelling in code --- .linter/custom_dict | 102 ++++++++++++++++------------- svg2mod/coloredlogger.py | 22 +++---- svg2mod/exporter.py | 8 +-- svg2mod/svg/geometry.py | 12 ++-- svg2mod/svg/svg.py | 134 +++++++++++++++++++-------------------- svg2mod/svg2mod.py | 24 +++---- 6 files changed, 158 insertions(+), 144 deletions(-) diff --git a/.linter/custom_dict b/.linter/custom_dict index be06750..662c54d 100644 --- a/.linter/custom_dict +++ b/.linter/custom_dict @@ -1,54 +1,68 @@ -svg -inkscape -kicad -stderr -stdout -vect -rx -ry -json -px +adhes +API +attrib bezier -txt cjlano -sys -Ramer +Cmap +cmts +collinear +coloredlogger +coord +copperpour +decompiles +Demibold +descr Douglas -Peucker -Traceback -precompute -pdistance +DPI +dwgs elt -rect -coord -pc -attrib -viewport -init -collinear -un +evenodd +Faux +fonttools formatter -pre +getLogger +init +inkscape +inlinable inlined -otf -ttf -xml iterable -decompiles -py -API -DPI -Wikipedia +json +keepout +kicad +kipart +levelno +otf +pc +PCBNEW +pdistance +Peucker poly +pre +precompute +px +py +pyenchant +Ramer +rect Regex -thru -Faux -getLogger -keepout -fonttools -adhes -dwgs -cmts -copperpour +rx +ry +stderr +stdout streamlitapp -evenodd \ No newline at end of file +svg +sys +tedit +thru +Traceback +ttf +txt +un +vect +viewport +Wikipedia +xlength +xml +xscale +ylength +yscale diff --git a/svg2mod/coloredlogger.py b/svg2mod/coloredlogger.py index a8d155d..5101e59 100644 --- a/svg2mod/coloredlogger.py +++ b/svg2mod/coloredlogger.py @@ -84,22 +84,22 @@ def format(self, record): #---------------------------------------------------------------------------- -def split_logger(logger, formatter=Formatter(), brkpoint=logging.WARNING): +def split_logger(logger, formatter=Formatter(), break_point=logging.WARNING): '''This will split logging messages at the specified break point. Anything higher will be sent to sys.stderr and everything else to sys.stdout ''' - for hndl in logger.handlers: - logger.removeHandler(hndl) + for handler in logger.handlers: + logger.removeHandler(handler) - hdlrerr = logging.StreamHandler(sys.stderr) - hdlrerr.addFilter(lambda msg: brkpoint <= msg.levelno) + handler_error = logging.StreamHandler(sys.stderr) + handler_error.addFilter(lambda msg: break_point <= msg.levelno) - hdlrout = logging.StreamHandler(sys.stdout) - hdlrout.addFilter(lambda msg: brkpoint > msg.levelno) + handler_out = logging.StreamHandler(sys.stdout) + handler_out.addFilter(lambda msg: break_point > msg.levelno) - hdlrerr.setFormatter(formatter) - hdlrout.setFormatter(formatter) - logger.addHandler(hdlrerr) - logger.addHandler(hdlrout) + handler_error.setFormatter(formatter) + handler_out.setFormatter(formatter) + logger.addHandler(handler_error) + logger.addHandler(handler_out) #---------------------------------------------------------------------------- \ No newline at end of file diff --git a/svg2mod/exporter.py b/svg2mod/exporter.py index 43c9bb3..5f78a53 100644 --- a/svg2mod/exporter.py +++ b/svg2mod/exporter.py @@ -1237,11 +1237,11 @@ def _write_polygon_header( self, points, layer, stroke_width): self.output_file.write('''\n (primitives\n (gr_poly (pts \n''') self._special_footer = " )\n (width {}){{2}})\n ))".format(stroke_width) - originx = points[0].x - originy = points[0].y + origin_x = points[0].x + origin_y = points[0].y for point in points: - point.x = point.x-originx - point.y = point.y-originy + point.x = point.x-origin_x + point.y = point.y-origin_y else: for point in points[:]: points.remove(point) diff --git a/svg2mod/svg/geometry.py b/svg2mod/svg/geometry.py index 7665fe8..a931281 100644 --- a/svg2mod/svg/geometry.py +++ b/svg2mod/svg/geometry.py @@ -128,9 +128,9 @@ def rot(self, angle, x=0, y=0): new_y = ((self.x-x) * angle.sin) + ((self.y-y) * angle.cos) + y return Point(new_x,new_y) - def round(self, ndigits=None): + def round(self, num_digits=None): '''Round x and y to number of decimal points''' - return Point( round(self.x, ndigits), round(self.y, ndigits)) + return Point( round(self.x, num_digits), round(self.y, num_digits)) class Angle: @@ -237,7 +237,7 @@ def control_point(self, n): raise LookupError('Index is larger than Bezier curve dimension') return self.pts[n] - def rlength(self): + def r_length(self): '''Rough Bezier length: length of control point segments''' pts = list(self.pts) l = 0.0 @@ -250,9 +250,9 @@ def rlength(self): def bbox(self): '''This returns the rough bounding box ''' - return self.rbbox() + return self.r_bbox() - def rbbox(self): + def r_bbox(self): '''Rough bounding box: return the bounding box (P1,P2) of the Bezier _control_ points''' xmin = min([p.x for p in self.pts]) @@ -268,7 +268,7 @@ def segments(self, precision=0): segments = [] # n is the number of Bezier points to draw according to precision if precision != 0: - n = int(self.rlength() / precision) + 1 + n = int(self.r_length() / precision) + 1 else: n = 1000 #if n < 10: n = 10 diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 6a0c6bc..d4d76b9 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -78,8 +78,8 @@ def __init__(self, elt=None, parent_styles=None): self.name = "" # Unit transformation matrix on init self.matrix = Matrix() - self.scalex = 1 - self.scaley = 1 + self.xscale = 1 + self.yscale = 1 self.style = {} if not parent_styles and not isinstance(parent_styles, dict) else parent_styles.copy() self.rotation = 0 self.viewport = Point(800, 600) # default viewport is 800x600 @@ -131,13 +131,13 @@ def parse_name( tag ): def bbox(self): '''Bounding box of all points''' - bboxes = [x.bbox() for x in self.items] - if len( bboxes ) < 1: + b_boxes = [x.bbox() for x in self.items] + if len( b_boxes ) < 1: return (Point(0, 0), Point(0, 0)) - xmin = min([b[0].x for b in bboxes]) - xmax = max([b[1].x for b in bboxes]) - ymin = min([b[0].y for b in bboxes]) - ymax = max([b[1].y for b in bboxes]) + xmin = min([b[0].x for b in b_boxes]) + xmax = max([b[1].x for b in b_boxes]) + ymin = min([b[0].y for b in b_boxes]) + ymax = max([b[1].y for b in b_boxes]) return (Point(xmin,ymin), Point(xmax,ymax)) @@ -178,18 +178,18 @@ def get_transformations(self, elt): sx = arg[0] if len(arg) == 1: sy = sx else: sy = arg[1] - self.scalex *= sx - self.scaley *= sy + self.xscale *= sx + self.yscale *= sy self.matrix *= Matrix([sx, 0, 0, sy, 0, 0]) if op == 'rotate': self.rotation += arg[0] - cosa = math.cos(math.radians(arg[0])) - sina = math.sin(math.radians(arg[0])) + cos_a = math.cos(math.radians(arg[0])) + sin_a = math.sin(math.radians(arg[0])) if len(arg) != 1: tx, ty = arg[1:3] self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) - self.matrix *= Matrix([cosa, sina, -sina, cosa, 0, 0]) + self.matrix *= Matrix([cos_a, sin_a, -sin_a, cos_a, 0, 0]) if len(arg) != 1: self.matrix *= Matrix([1, 0, 0, 1, -tx, -ty]) @@ -459,31 +459,31 @@ def __init__(self, elt=None, *args, **kwargs): if elt is not None: self.parse(elt.get('d')) - def parse(self, pathstr:str): + def parse(self, path_str:str): """Parse svg path string and build elements list""" - pathlst = re.findall(number_re + r"|\ *[%s]\ *" % Path.COMMANDS, pathstr) + path_list = re.findall(number_re + r"|\ *[%s]\ *" % Path.COMMANDS, path_str) - pathlst.reverse() + path_list.reverse() command = None current_pt = Point(0,0) start_pt = None - while pathlst: - if pathlst[-1].strip() in Path.COMMANDS: + while path_list: + if path_list[-1].strip() in Path.COMMANDS: last_command = command - command = pathlst.pop().strip() + command = path_list.pop().strip() absolute = (command == command.upper()) command = command.upper() else: if command is None: - raise ValueError("No command found at %d" % len(pathlst)) + raise ValueError("No command found at %d" % len(path_list)) if command == 'M': # MoveTo - x = pathlst.pop() - y = pathlst.pop() + x = path_list.pop() + y = path_list.pop() pt = Point(x, y) if absolute: current_pt = pt @@ -511,9 +511,9 @@ def parse(self, pathstr:str): x,y = (0,0) if command in 'LH': - x = pathlst.pop() + x = path_list.pop() if command in 'LV': - y = pathlst.pop() + y = path_list.pop() pt = Point(x, y) if not absolute: @@ -527,8 +527,8 @@ def parse(self, pathstr:str): bezier_pts = [] bezier_pts.append(current_pt) for _ in range(1,dimension[command]): - x = pathlst.pop() - y = pathlst.pop() + x = path_list.pop() + y = path_list.pop() pt = Point(x, y) if not absolute: pt += current_pt @@ -539,9 +539,9 @@ def parse(self, pathstr:str): elif command in 'TS': # number of points to read - nbpts = {'T':1, 'S':2} + num_pts = {'T':1, 'S':2} # the control point, from previous Bezier to mirror - ctrlpt = {'T':1, 'S':2} + ctrl_pt = {'T':1, 'S':2} # last command control last = {'T': 'QT', 'S':'CS'} @@ -549,16 +549,16 @@ def parse(self, pathstr:str): bezier_pts.append(current_pt) if last_command in last[command]: - pt0 = self.items[-1].control_point(ctrlpt[command]) + pt0 = self.items[-1].control_point(ctrl_pt[command]) else: pt0 = current_pt pt1 = current_pt # Symmetrical of pt1 against pt0 bezier_pts.append(pt1 + pt1 - pt0) - for _ in range(0,nbpts[command]): - x = pathlst.pop() - y = pathlst.pop() + for _ in range(0,num_pts[command]): + x = path_list.pop() + y = path_list.pop() pt = Point(x, y) if not absolute: pt += current_pt @@ -568,34 +568,34 @@ def parse(self, pathstr:str): current_pt = pt elif command == 'A': - rx = pathlst.pop() - ry = pathlst.pop() - xrot = pathlst.pop() + rx = path_list.pop() + ry = path_list.pop() + x_rotation = path_list.pop() # Arc flags are not necessarily separated numbers - flags = pathlst.pop().strip() + flags = path_list.pop().strip() large_arc_flag = flags[0] if large_arc_flag not in '01': logger.error("Arc parsing failure") break if len(flags) > 1: flags = flags[1:].strip() - else: flags = pathlst.pop().strip() + else: flags = path_list.pop().strip() sweep_flag = flags[0] if sweep_flag not in '01': logger.error("Arc parsing failure") break if len(flags) > 1: x = flags[1:] - else: x = pathlst.pop() - y = pathlst.pop() + else: x = path_list.pop() + y = path_list.pop() end_pt = Point(x, y) if not absolute: end_pt += current_pt self.items.append( - Arc(current_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt)) + Arc(current_pt, rx, ry, x_rotation, large_arc_flag, sweep_flag, end_pt)) current_pt = end_pt else: - pathlst.pop() + path_list.pop() def __str__(self): return '\n'.join(str(x) for x in self.items) @@ -722,7 +722,7 @@ def bbox(self) -> Tuple[Point, Point]: ''' if self.arc: return Transformable.bbox(self) - + points = self.segments((self.rx+self.ry) / 8) points = list(itertools.chain.from_iterable(points)) @@ -762,8 +762,8 @@ def P(self, t) -> Point: def segments(self, precision=0) -> List[Segment]: '''Flatten all curves to segments with target length of precision''' if self.arc: - segs = self.path.segments(precision) - return segs + segments = self.path.segments(precision) + return segments if max(self.rx, self.ry) < precision: return [[self.center]] @@ -791,12 +791,12 @@ class Arc(Ellipse): path data for an arc into an object that can be flattened. ''' - def __init__(self, start_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt): + def __init__(self, start_pt, rx, ry, x_rotation, large_arc_flag, sweep_flag, end_pt): Ellipse.__init__(self, None) try: self.rx = float(rx) self.ry = float(ry) - self.rotation = float(xrot) + self.rotation = float(x_rotation) self.large_arc_flag = large_arc_flag=='1' self.sweep_flag = sweep_flag=='1' except: @@ -804,12 +804,12 @@ def __init__(self, start_pt, rx, ry, xrot, large_arc_flag, sweep_flag, end_pt): self.end_pts = [start_pt, end_pt] self.angles = [] - self.calcuate_center() + self.calculate_center() def __repr__(self): return '' - def calcuate_center(self): + def calculate_center(self): '''Calculate the center point of the arc from the non-intuitively provided data in an svg path. @@ -887,12 +887,12 @@ def calcuate_center(self): # finish solving the quadratic equation and find the corresponding points on the intersection line elif root == 0: - xroot = (-qb+math.sqrt(root))/(2*qa) - point = Point(xroot, xroot*m + b) + x_root = (-qb+math.sqrt(root))/(2*qa) + point = Point(x_root, x_root*m + b) # Using the provided large_arc and sweep flags to choose the correct root else: - xroots = [(-qb+math.sqrt(root))/(2*qa), (-qb-math.sqrt(root))/(2*qa)] - points = [Point(xroots[0], xroots[0]*m + b), Point(xroots[1], xroots[1]*m + b)] + x_roots = [(-qb+math.sqrt(root))/(2*qa), (-qb-math.sqrt(root))/(2*qa)] + points = [Point(x_roots[0], x_roots[0]*m + b), Point(x_roots[1], x_roots[1]*m + b)] # Calculate the angle of the beginning point to the end point # If counterclockwise the two angles are the angle is within 180 degrees of each other: @@ -1332,10 +1332,10 @@ def convert_to_path(self, auto_transform=True): path = [] for char in text: - pathbuf = "" + path_buff = "" try: glf = ttf.getGlyphSet()[ttf.getBestCmap()[ord(char)]] except KeyError: - logger.warning('Unsuported character in element "{}"'.format(char)) + logger.warning('Unsupported character in element "{}"'.format(char)) #txt = txt.replace(char, "") continue @@ -1343,11 +1343,11 @@ def convert_to_path(self, auto_transform=True): glf.draw(pen) for cmd in pen._commands: - pathbuf += cmd + ' ' + path_buff += cmd + ' ' - if len(pathbuf) > 0: + if len(path_buff) > 0: path.append(Path()) - path[-1].parse(pathbuf) + path[-1].parse(path_buff) # Apply the scaling then the translation translate = Matrix([1,0,0,-1,offset.x,size+attrib.origin.y]) * Matrix([scale,0,0,scale,0,0]) # This queues the translations until .transform() is called @@ -1367,11 +1367,11 @@ def bbox(self) -> Tuple[Point, Point]: if self.paths is None or len(self.paths) == 0: return [Point(0,0),Point(0,0)] - bboxes = [path.bbox() for paths in self.paths for path in paths] + b_boxes = [path.bbox() for paths in self.paths for path in paths] return ( - Point(min(bboxes, key=lambda v: v[0].x)[0].x, min(bboxes, key=lambda v: v[0].y)[0].y), - Point(max(bboxes, key=lambda v: v[1].x)[1].x, max(bboxes, key=lambda v: v[1].y)[1].y), + Point(min(b_boxes, key=lambda v: v[0].x)[0].x, min(b_boxes, key=lambda v: v[0].y)[0].y), + Point(max(b_boxes, key=lambda v: v[1].x)[1].x, max(b_boxes, key=lambda v: v[1].y)[1].y), ) def transform(self, matrix=None): @@ -1395,11 +1395,11 @@ def segments(self, precision=0) -> List[Segment]: with provide precision. This will only work if there are available paths. ''' - segs = [] + segments = [] for paths in self.paths: for path in paths: - segs.extend(path.segments(precision)) - return segs + segments.extend(path.segments(precision)) + return segments @staticmethod def load_system_fonts(reload:bool=False) -> List[dict]: @@ -1423,15 +1423,15 @@ def load_system_fonts(reload:bool=False) -> List[dict]: except: pass - for ffile in fonts_files: + for font_file in fonts_files: try: - font = ttFont.TTFont(ffile) + font = ttFont.TTFont(font_file) name = font["name"].getName(1,1,0).toStr() style = font["name"].getName(2,1,0).toStr() if Text._system_fonts.get(name) is None: - Text._system_fonts[name] = {style:ffile} + Text._system_fonts[name] = {style:font_file} elif Text._system_fonts[name].get(style) is None: - Text._system_fonts[name][style] = ffile + Text._system_fonts[name][style] = font_file except: pass logger.debug(f" Found {len(Text._system_fonts.keys())} fonts in system") diff --git a/svg2mod/svg2mod.py b/svg2mod/svg2mod.py index 41087b8..f6e63df 100755 --- a/svg2mod/svg2mod.py +++ b/svg2mod/svg2mod.py @@ -357,8 +357,8 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int hole_segment = LineSegment() intersections = 0 - intersect_segs = [] - virt_line = LineSegment() + intersect_segments = [] + virtual_line = LineSegment() # Check each segment of other hole for intersection: for point in self.points: @@ -374,7 +374,7 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int if count_intersections: if get_points: - intersect_segs.append((hole_segment.p, hole_segment.q)) + intersect_segments.append((hole_segment.p, hole_segment.q)) else: # If a point is on the line segment we need to see if the # simplified "virtual" line crosses the line segment. @@ -382,20 +382,20 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int # Set the endpoints if they are of the line segment if line_segment.on_line(hole_segment.q): if not line_segment.on_line(hole_segment.p): - virt_line.p = hole_segment.p + virtual_line.p = hole_segment.p elif line_segment.on_line(hole_segment.p): - virt_line.q = hole_segment.q + virtual_line.q = hole_segment.q # No points are on the line segment else: intersections += 1 - virt_line = LineSegment() + virtual_line = LineSegment() # The virtual line is complete check for intersections - if virt_line.p and virt_line.q: - if virt_line.intersects(line_segment): + if virtual_line.p and virtual_line.q: + if virtual_line.intersects(line_segment): intersections += 1 - virt_line = LineSegment() + virtual_line = LineSegment() elif get_points: return hole_segment.p, hole_segment.q @@ -403,7 +403,7 @@ def intersects( self, line_segment: LineSegment, check_connects:bool , count_int return True if count_intersections: - return intersect_segs if get_points else intersections + return intersect_segments if get_points else intersections if get_points and not check_connects: return () return False @@ -472,8 +472,8 @@ def are_distinct(self, polygon): # Check number of horizontal intersections. If the number is odd then it the smaller polygon # is contained. If the number is even then the polygon is outside of the larger polygon if not distinct: - tline = LineSegment(smaller.points[0], svg.Point(larger.bbox[1].x+1, smaller.points[0].y)) - distinct = bool((larger.intersects(tline, False, True) + 1)%2) + test_line = LineSegment(smaller.points[0], svg.Point(larger.bbox[1].x+1, smaller.points[0].y)) + distinct = bool((larger.intersects(test_line, False, True) + 1)%2) return distinct #------------------------------------------------------------------------ From 5a65c664c942f3c5cabc495e61c450ce63f675f1 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 1 Nov 2022 16:20:15 -0600 Subject: [PATCH 145/151] Fix bug with arc bounding box --- svg2mod/svg/svg.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index d4d76b9..f60ee62 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -31,7 +31,7 @@ import re import sys import xml.etree.ElementTree as etree -from typing import List, Tuple +from typing import Iterable, List, Tuple from fontTools.misc import loggingTools from fontTools.pens.svgPathPen import SVGPathPen @@ -724,7 +724,8 @@ def bbox(self) -> Tuple[Point, Point]: return Transformable.bbox(self) points = self.segments((self.rx+self.ry) / 8) - points = list(itertools.chain.from_iterable(points)) + if isinstance(points[0], Iterable): + points = list(itertools.chain.from_iterable(points)) xmin = min([p.x for p in points]) xmax = max([p.x for p in points]) From 817be915859e9d9a8b975d4ae7f52ee3ccae596a Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 7 Nov 2022 22:12:05 -0700 Subject: [PATCH 146/151] Finish polygon support and better style supports. --- README.md | 3 ++- svg2mod/svg/svg.py | 45 ++++++++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d92292f..9d761df 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,10 @@ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/svg2mod?style=for-the-badge)](https://pypi.org/project/svg2mod/) -[![GitHub Sponsors](https://img.shields.io/github/sponsors/sodium-hydrogen?logo=github&style=for-the-badge)](https://github.com/sponsors/Sodium-Hydrogen) [![PyPI - License](https://img.shields.io/pypi/l/svg2mod?color=purple&style=for-the-badge)](https://pypi.org/project/svg2mod/) +[![GitHub Sponsors](https://img.shields.io/github/sponsors/sodium-hydrogen?logo=github&style=for-the-badge&color=red)](https://github.com/sponsors/Sodium-Hydrogen) + This is a program / library to convert SVG drawings to KiCad footprint module files. It includes a modified version of [cjlano's python SVG parser and drawing module](https://github.com/cjlano/svg) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index f60ee62..e9a6e47 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -46,6 +46,15 @@ number_re = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?' unit_re = r'em|ex|px|in|cm|mm|pt|pc|%' +# styles of interest and their defaults +svg_defaults = { + "fill":"black", + "fill-opacity":"1", + "stroke":"none", + "stroke-width":"1px", + "stroke-opacity":"1", + } + # Unit converter unit_convert = { None: 1, # Default unit (same as pixel) @@ -76,11 +85,12 @@ def __init__(self, elt=None, parent_styles=None): self.items = [] self.id = hex(id(self)) self.name = "" + self.fill_even_odd = False # Unit transformation matrix on init self.matrix = Matrix() self.xscale = 1 self.yscale = 1 - self.style = {} if not parent_styles and not isinstance(parent_styles, dict) else parent_styles.copy() + self.style = svg_defaults.copy() if not parent_styles and not isinstance(parent_styles, dict) else parent_styles.copy() self.rotation = 0 self.viewport = Point(800, 600) # default viewport is 800x600 if elt is not None: @@ -97,6 +107,15 @@ def __init__(self, elt=None, parent_styles=None): if self.name == '': self.name == self.id + # set fill_even_odd if property set + self.fill_even_odd = elt.get("fill-rule", '').lower() == 'evenodd' + if self.fill_even_odd: + logger.warning(f"Found unsupported attribute: 'fill-rule=evenodd' for {repr(self)}") + + # Find attributes of interest. The are overwritten by styles + for style_key in svg_defaults: + self.style[style_key] = elt.get(style_key, self.style[style_key]) + # parse styles and save as dictionary. if elt.get('style'): for style in elt.get('style').split(";"): @@ -204,10 +223,16 @@ def get_transformations(self, elt): def transform_styles(self, matrix): '''Any style in this classes transformable_styles will be scaled by the provided matrix. + If it has a unit type it will convert it to the proper value first. ''' for style in self.transformable_styles: if self.style.get(style): - self.style[style] = float(self.style[style]) * ((matrix.xscale()+matrix.yscale())/2) + has_units = re.search(r'\D', self.style[style] if isinstance(self.style[style], str) else '') + if has_units is None: + self.style[style] = float(self.style[style]) * ((matrix.xscale()+matrix.yscale())/2) + else: + unit = has_units.group().lower() + self.style[style] = float(re.search(r'\d', self.style[style]).group()) * unit_convert.get(unit, 1) * ((matrix.xscale()+matrix.yscale())/2) def transform(self, matrix=None): @@ -628,12 +653,9 @@ def simplify(self, precision:float) -> List[Segment]: return ret -class Polygon(Transformable): +class Polygon(Path): '''SVG tag handler A polygon has a space separated list of points in format x,y. - - Additionally, polygons can have a style of `file-rule: evenodd;` - which allows intersections to cause the polygon to have holes. ''' # class Polygon handles the tag @@ -646,7 +668,6 @@ def __init__(self, elt, *args, **kwargs): if elt.get('pathLength'): self.path_len = int(elt.get('pathLength')) self.parse(elt.get('points')) - logger.warning("Polygons are partially supported and may not give expected results.") def parse(self, point_str): '''Split the points from point_str and create a list of segments''' @@ -662,22 +683,16 @@ def parse(self, point_str): if start_pt and current_pt: self.items.append(Segment(start_pt, current_pt)) - def __str__(self): - return ' '.join(str(x) for x in self.items) - def __repr__(self) -> str: return '' def segments(self, precision=0) -> List[Segment]: - '''Simplify segments if evenodd then cutout holes''' + ''' Return list of segments ''' + seg = [x.segments(precision) for x in self.items] return [list(itertools.chain.from_iterable(seg))] - def simplify(self, precision:float) -> List[Segment]: - '''Simplify segments if evenodd then cutout holes: - Remove any point which are ~aligned''' - return [simplify_segment(self.segments()[0], precision)] class Ellipse(Transformable): '''SVG tag handler From d912ff2c7733f06cbc22fb42e24e1883884ae313 Mon Sep 17 00:00:00 2001 From: Greg Davill Date: Sun, 20 Nov 2022 23:28:34 +1030 Subject: [PATCH 147/151] svg.text: Pass through glyphSet not method --- svg2mod/svg/svg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index e9a6e47..074ce4e 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -1355,7 +1355,7 @@ def convert_to_path(self, auto_transform=True): #txt = txt.replace(char, "") continue - pen = SVGPathPen(ttf.getGlyphSet) + pen = SVGPathPen(ttf.getGlyphSet()) glf.draw(pen) for cmd in pen._commands: From d9b5dd02a9d37ff725f4f2c53b55ddd49a44b5ec Mon Sep 17 00:00:00 2001 From: Sodium-Hydrogen Date: Wed, 1 Feb 2023 14:49:59 -0700 Subject: [PATCH 148/151] Fix auto build badge See badges/shields#8671 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d761df..ac05603 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # svg2mod -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/svg2mod/svg2mod/Python%20lint%20and%20test?logo=github&style=for-the-badge)](https://github.com/svg2mod/svg2mod/actions/workflows/python-package.yml) +[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/svg2mod/svg2mod/python-package.yml?branch=main&logo=github&style=for-the-badge)](https://github.com/svg2mod/svg2mod/actions/workflows/python-package.yml) [![GitHub last commit](https://img.shields.io/github/last-commit/svg2mod/svg2mod?style=for-the-badge)](https://github.com/svg2mod/svg2mod/commits/main) [![PyPI](https://img.shields.io/pypi/v/svg2mod?color=informational&label=version&style=for-the-badge)](https://pypi.org/project/svg2mod/) From 19579d76f6f2fb64b222ca38fe81a73c448244bc Mon Sep 17 00:00:00 2001 From: ivan-the-terrible Date: Mon, 24 Feb 2025 15:12:05 -0500 Subject: [PATCH 149/151] fix(regex units and matrix scaling) --- svg2mod/svg/svg.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 074ce4e..2e939b8 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -36,6 +36,7 @@ from fontTools.misc import loggingTools from fontTools.pens.svgPathPen import SVGPathPen from fontTools.ttLib import ttFont + from svg2mod.coloredlogger import logger from .geometry import Angle, Bezier, MoveTo, Point, Segment, simplify_segment @@ -227,12 +228,12 @@ def transform_styles(self, matrix): ''' for style in self.transformable_styles: if self.style.get(style): - has_units = re.search(r'\D', self.style[style] if isinstance(self.style[style], str) else '') + has_units = re.search(r'\D+', self.style[style] if isinstance(self.style[style], str) else '') if has_units is None: self.style[style] = float(self.style[style]) * ((matrix.xscale()+matrix.yscale())/2) else: unit = has_units.group().lower() - self.style[style] = float(re.search(r'\d', self.style[style]).group()) * unit_convert.get(unit, 1) * ((matrix.xscale()+matrix.yscale())/2) + self.style[style] = float(re.search(r'\d+', self.style[style]).group()) * unit_convert.get(unit, 1) * ((matrix.xscale()+matrix.yscale())/2) def transform(self, matrix=None): @@ -411,13 +412,14 @@ def json(self): return {'Group ' + self.id + " ({})".format( self.name ) : self.items} class Matrix: - ''' SVG transformation matrix and its operations + '''SVG transformation matrix and its operations a SVG matrix is represented as a list of 6 values [a, b, c, d, e, f] (named vect hereafter) which represent the 3x3 matrix ((a, c, e) (b, d, f) (0, 0, 1)) - see http://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace ''' + SVGs implement the same transform that CSS does + see https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix#values''' def __init__(self, vect=None): # Unit transformation vect by default @@ -452,10 +454,16 @@ def __str__(self): def xscale(self): '''Return the rotated x scalar value''' - return self.vect[0]/abs(self.vect[0]) * math.sqrt(self.vect[0]**2 + self.vect[2]**2) + if self.vect[0] == 0: + return 1 + else: + return self.vect[0]/abs(self.vect[0]) * math.sqrt(self.vect[0]**2 + self.vect[2]**2) def yscale(self): '''Return the rotated x scalar value''' - return self.vect[3]/abs(self.vect[3]) * math.sqrt(self.vect[1]**2 + self.vect[3]**2) + if self.vect[3] == 0: + return 1 + else: + return self.vect[3]/abs(self.vect[3]) * math.sqrt(self.vect[1]**2 + self.vect[3]**2) def rot(self): '''Return the angle of rotation from the matrix. @@ -468,7 +476,6 @@ def rot(self): return 0 - class Path(Transformable): '''SVG tag handler self.items contains all objects for path instructions. @@ -979,7 +986,6 @@ def P(self, t) -> Point: return Point(x,y) - # A circle is a special type of ellipse where rx = ry = radius class Circle(Ellipse): '''SVG tag handler From ab2f5810688e2f9c083a61ce2d41da4802a6ebfd Mon Sep 17 00:00:00 2001 From: ivan-the-terrible Date: Mon, 24 Feb 2025 20:48:45 -0500 Subject: [PATCH 150/151] fix(updated logic on default case for scaling functions and fixed typo in doc string) --- svg2mod/svg/svg.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 2e939b8..7f93fa1 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -455,13 +455,13 @@ def __str__(self): def xscale(self): '''Return the rotated x scalar value''' if self.vect[0] == 0: - return 1 + return abs(self.vect[2]) else: return self.vect[0]/abs(self.vect[0]) * math.sqrt(self.vect[0]**2 + self.vect[2]**2) def yscale(self): - '''Return the rotated x scalar value''' + '''Return the rotated y scalar value''' if self.vect[3] == 0: - return 1 + return abs(self.vect[1]) else: return self.vect[3]/abs(self.vect[3]) * math.sqrt(self.vect[1]**2 + self.vect[3]**2) def rot(self): @@ -1487,4 +1487,3 @@ def default(self, obj): tag = getattr(cls, 'tag', None) if tag: svgClass[svg_ns + tag] = cls - From 8bc51ed37d6efcdc477259c9bda2a1d51c32dc69 Mon Sep 17 00:00:00 2001 From: ivan-the-terrible Date: Sun, 2 Mar 2025 20:18:55 -0500 Subject: [PATCH 151/151] fix(update scaling technique to use absolute values) --- svg2mod/svg/svg.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/svg2mod/svg/svg.py b/svg2mod/svg/svg.py index 7f93fa1..baa4d43 100644 --- a/svg2mod/svg/svg.py +++ b/svg2mod/svg/svg.py @@ -229,11 +229,13 @@ def transform_styles(self, matrix): for style in self.transformable_styles: if self.style.get(style): has_units = re.search(r'\D+', self.style[style] if isinstance(self.style[style], str) else '') + xscale = abs(matrix.xscale()) + yscale = abs(matrix.yscale()) if has_units is None: - self.style[style] = float(self.style[style]) * ((matrix.xscale()+matrix.yscale())/2) + self.style[style] = float(self.style[style]) * ((xscale+yscale)/2) else: unit = has_units.group().lower() - self.style[style] = float(re.search(r'\d+', self.style[style]).group()) * unit_convert.get(unit, 1) * ((matrix.xscale()+matrix.yscale())/2) + self.style[style] = float(re.search(r'\d+', self.style[style]).group()) * unit_convert.get(unit, 1) * ((xscale+yscale)/2) def transform(self, matrix=None):