From 69d24a075ab8704f21e84ee9d6af281d1348e1a0 Mon Sep 17 00:00:00 2001 From: Uri Shaked Date: Tue, 15 Jan 2019 00:08:03 +0200 Subject: [PATCH] feat(pcb): add modified version of svg2mod Includes Python 3 fixes and some of the changes from: https://github.com/mtl/svg2mod/pull/26 --- pcb/svg2mod/.gitignore | 1 + pcb/svg2mod/__init__.py | 0 pcb/svg2mod/svg/LICENSE | 339 +++++++ pcb/svg2mod/svg/README.md | 18 + pcb/svg2mod/svg/__init__.py | 1 + pcb/svg2mod/svg/svg/__init__.py | 8 + pcb/svg2mod/svg/svg/geometry.py | 343 ++++++++ pcb/svg2mod/svg/svg/svg.py | 711 +++++++++++++++ pcb/svg2mod/svg2mod.py | 1465 +++++++++++++++++++++++++++++++ 9 files changed, 2886 insertions(+) create mode 100644 pcb/svg2mod/.gitignore create mode 100644 pcb/svg2mod/__init__.py create mode 100644 pcb/svg2mod/svg/LICENSE create mode 100644 pcb/svg2mod/svg/README.md create mode 100644 pcb/svg2mod/svg/__init__.py create mode 100644 pcb/svg2mod/svg/svg/__init__.py create mode 100644 pcb/svg2mod/svg/svg/geometry.py create mode 100644 pcb/svg2mod/svg/svg/svg.py create mode 100644 pcb/svg2mod/svg2mod.py diff --git a/pcb/svg2mod/.gitignore b/pcb/svg2mod/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/pcb/svg2mod/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/pcb/svg2mod/__init__.py b/pcb/svg2mod/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pcb/svg2mod/svg/LICENSE b/pcb/svg2mod/svg/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/pcb/svg2mod/svg/LICENSE @@ -0,0 +1,339 @@ + 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/pcb/svg2mod/svg/README.md b/pcb/svg2mod/svg/README.md new file mode 100644 index 0000000..6ab06cd --- /dev/null +++ b/pcb/svg2mod/svg/README.md @@ -0,0 +1,18 @@ +SVG parser library +================== + +This is a SVG parser library written in Python. +([see here](https://github.com/cjlano/svg])) + +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: + - SVG Path Arc ('A') + - Non-linear transformation drawing (SkewX, ...) + +License: GPLv2+ diff --git a/pcb/svg2mod/svg/__init__.py b/pcb/svg2mod/svg/__init__.py new file mode 100644 index 0000000..b3c8618 --- /dev/null +++ b/pcb/svg2mod/svg/__init__.py @@ -0,0 +1 @@ +from .svg import * diff --git a/pcb/svg2mod/svg/svg/__init__.py b/pcb/svg2mod/svg/svg/__init__.py new file mode 100644 index 0000000..3090eb7 --- /dev/null +++ b/pcb/svg2mod/svg/svg/__init__.py @@ -0,0 +1,8 @@ +#__all__ = ['geometry', 'svg'] + +from .svg import * + +def parse(filename): + f = svg.Svg(filename) + return f + diff --git a/pcb/svg2mod/svg/svg/geometry.py b/pcb/svg2mod/svg/svg/geometry.py new file mode 100644 index 0000000..7a4114f --- /dev/null +++ b/pcb/svg2mod/svg/svg/geometry.py @@ -0,0 +1,343 @@ +# Copyright (C) 2013 -- CJlano < cjlano @ free.fr > + +# 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 all the geometric classes and functions not directly +related to SVG parsing. It can be reused outside the scope of SVG. +''' + +import math +import numbers +import operator + +class Point: + def __init__(self, x=None, y=None): + '''A Point is defined either by a tuple/list of length 2 or + by 2 coordinates + >>> Point(1,2) + (1.000,2.000) + >>> Point((1,2)) + (1.000,2.000) + >>> Point([1,2]) + (1.000,2.000) + >>> Point('1', '2') + (1.000,2.000) + >>> Point(('1', None)) + (1.000,0.000) + ''' + if (isinstance(x, tuple) or isinstance(x, list)) and len(x) == 2: + x,y = x + + # Handle empty parameter(s) which should be interpreted as 0 + if x is None: x = 0 + if y is None: y = 0 + + try: + self.x = float(x) + self.y = float(y) + except: + raise TypeError("A Point is defined by 2 numbers or a tuple") + + def __add__(self, other): + '''Add 2 points by adding coordinates. + Try to convert other to Point if necessary + >>> Point(1,2) + Point(3,2) + (4.000,4.000) + >>> Point(1,2) + (3,2) + (4.000,4.000)''' + if not isinstance(other, Point): + try: other = Point(other) + except: return NotImplemented + return Point(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + '''Substract two Points. + >>> Point(1,2) - Point(3,2) + (-2.000,0.000) + ''' + if not isinstance(other, Point): + try: other = Point(other) + except: return NotImplemented + return Point(self.x - other.x, self.y - other.y) + + def __mul__(self, other): + '''Multiply a Point with a constant. + >>> 2 * Point(1,2) + (2.000,4.000) + >>> Point(1,2) * Point(1,2) #doctest:+IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError: + ''' + if not isinstance(other, numbers.Real): + return NotImplemented + return Point(self.x * other, self.y * other) + def __rmul__(self, other): + return self.__mul__(other) + + def __eq__(self, other): + '''Test equality + >>> Point(1,2) == (1,2) + True + >>> Point(1,2) == Point(2,1) + False + ''' + if not isinstance(other, Point): + try: other = Point(other) + except: return NotImplemented + return (self.x == other.x) and (self.y == other.y) + + def __repr__(self): + return '(' + format(self.x,'.3f') + ',' + format( self.y,'.3f') + ')' + + def __str__(self): + return self.__repr__(); + + def coord(self): + '''Return the point tuple (x,y)''' + return (self.x, self.y) + + def length(self): + '''Vector length, Pythagoras theorem''' + return math.sqrt(self.x ** 2 + self.y ** 2) + + def rot(self, angle): + '''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) + + +class Angle: + '''Define a trigonometric angle [of a vector] ''' + def __init__(self, arg): + if isinstance(arg, numbers.Real): + # We precompute sin and cos for rotations + self.angle = arg + self.cos = math.cos(self.angle) + self.sin = math.sin(self.angle) + elif isinstance(arg, Point): + # Point angle is the trigonometric angle of the vector [origin, Point] + pt = arg + try: + self.cos = pt.x/pt.length() + self.sin = pt.y/pt.length() + except ZeroDivisionError: + self.cos = 1 + self.sin = 0 + + self.angle = math.acos(self.cos) + if self.sin < 0: + self.angle = -self.angle + else: + raise TypeError("Angle is defined by a number or a Point") + + def __neg__(self): + return Angle(Point(self.cos, -self.sin)) + +class Segment: + '''A segment is an object defined by 2 points''' + def __init__(self, start, end): + self.start = start + self.end = end + + def __str__(self): + return 'Segment from ' + str(self.start) + ' to ' + str(self.end) + + def segments(self, precision=0): + ''' Segments is simply the segment start -> end''' + return [self.start, self.end] + + def length(self): + '''Segment length, Pythagoras theorem''' + s = self.end - self.start + return math.sqrt(s.x ** 2 + s.y ** 2) + + def pdistance(self, p): + '''Perpendicular distance between this Segment and a given Point p''' + if not isinstance(p, Point): + return NotImplemented + + if self.start == self.end: + # Distance from a Point to another Point is length of a segment + return Segment(self.start, p).length() + + s = self.end - self.start + 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 formulae (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) + + + def bbox(self): + xmin = min(self.start.x, self.end.x) + xmax = max(self.start.x, self.end.x) + ymin = min(self.start.y, self.end.y) + ymax = max(self.start.y, self.end.y) + + return (Point(xmin,ymin),Point(xmax,ymax)) + + 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 + Its dimension is equal to the number of control points + Note that SVG only support dimension 3 and 4 Bezier curve, respectively + Quadratic and Cubic Bezier curve''' + def __init__(self, pts): + self.pts = list(pts) + self.dimension = len(pts) + + def __str__(self): + return 'Bezier' + str(self.dimension) + \ + ' : ' + ", ".join([str(x) for x in self.pts]) + + def control_point(self, n): + if n >= self.dimension: + raise LookupError('Index is larger than Bezier curve dimension') + else: + return self.pts[n] + + def rlength(self): + '''Rough Bezier length: length of control point segments''' + pts = list(self.pts) + l = 0.0 + p1 = pts.pop() + while pts: + p2 = pts.pop() + l += Segment(p1, p2).length() + p1 = p2 + return l + + def bbox(self): + return self.rbbox() + + def rbbox(self): + '''Rough bounding box: return the bounding box (P1,P2) of the Bezier + _control_ points''' + xmin = min([p.x for p in self.pts]) + xmax = max([p.x for p in self.pts]) + ymin = min([p.y for p in self.pts]) + ymax = max([p.y for p in self.pts]) + + return (Point(xmin,ymin), Point(xmax,ymax)) + + def segments(self, precision=0): + '''Return a polyline 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 + if precision != 0: + n = int(self.rlength() / precision) + 1 + else: + n = 1000 + #if n < 10: n = 10 + if n > 1000 : n = 1000 + + for t in range(0, n+1): + segments.append(self._bezierN(float(t)/n)) + return segments + + def _bezier1(self, p0, p1, t): + '''Bezier curve, one dimension + Compute the Point corresponding to a linear Bezier curve between + p0 and p1 at "time" t ''' + pt = p0 + t * (p1 - p0) + return pt + + def _bezierN(self, t): + '''Bezier curve, Nth dimension + Compute the point of the Nth dimension Bezier curve at "time" t''' + # We reduce the N Bezier control points by computing the linear Bezier + # point of each control point segment, creating N-1 control points + # until we reach one single point + res = list(self.pts) + # We store the resulting Bezier points in res[], recursively + for n in range(self.dimension, 1, -1): + # 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) + return res[0] + + 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 + + def bbox(self): + return (self.dest, self.dest) + + 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''' + if len(segment) < 3 or epsilon <= 0: + return segment[:] + + l = Segment(segment[0], segment[-1]) # Longest segment + + # Find the furthest point from the segment + index, maxDist = max([(i, l.pdistance(p)) for i,p in enumerate(segment)], + key=operator.itemgetter(1)) + + if maxDist > epsilon: + # Recursively call with segment splited in 2 on its furthest point + r1 = simplify_segment(segment[:index+1], epsilon) + r2 = simplify_segment(segment[index:], epsilon) + # Remove redundant 'middle' Point + return r1[:-1] + r2 + else: + return [segment[0], segment[-1]] diff --git a/pcb/svg2mod/svg/svg/svg.py b/pcb/svg2mod/svg/svg/svg.py new file mode 100644 index 0000000..02949d2 --- /dev/null +++ b/pcb/svg2mod/svg/svg/svg.py @@ -0,0 +1,711 @@ +# SVG parser in Python + +# Copyright (C) 2013 -- CJlano < cjlano @ free.fr > + +# 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. + +from __future__ import absolute_import +import traceback +import sys +import os +import copy +import re +import xml.etree.ElementTree as etree +import itertools +import operator +import json +from .geometry import * + + +svg_ns = '{http://www.w3.org/2000/svg}' + +# Regex commonly used +number_re = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?' +unit_re = r'em|ex|px|in|cm|mm|pt|pc|%' + +# Unit converter +unit_convert = { + None: 1, # Default unit (same as pixel) + 'px': 1, # px: pixel. Default SVG unit + 'em': 10, # 1 em = 10 px FIXME + 'ex': 5, # 1 ex = 5 px FIXME + 'in': 96, # 1 in = 96 px + 'cm': 96 / 2.54, # 1 cm = 1/2.54 in + 'mm': 96 / 25.4, # 1 mm = 1/25.4 in + 'pt': 96 / 72.0, # 1 pt = 1/72 in + 'pc': 96 / 6.0, # 1 pc = 1/6 in + '%' : 1 / 100.0 # 1 percent + } + +class Transformable: + '''Abstract class for objects that can be geometrically drawn & transformed''' + def __init__(self, elt=None): + # a 'Transformable' is represented as a list of Transformable items + self.items = [] + self.id = hex(id(self)) + # Unit transformation matrix on init + self.matrix = Matrix() + 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 + self.getTransformations(elt) + + def bbox(self): + '''Bounding box''' + bboxes = [x.bbox() for x in self.items] + if len( bboxes ) < 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]) + + return (Point(xmin,ymin), Point(xmax,ymax)) + + # Parse transform field + def getTransformations(self, elt): + t = elt.get('transform') + if t is None: return + + svg_transforms = [ + 'matrix', 'translate', 'scale', 'rotate', 'skewX', 'skewY'] + + # match any SVG transformation with its parameter (until final parenthese) + # [^)]* == anything but a closing parenthese + # '|'.join == OR-list of SVG transformations + transforms = re.findall( + '|'.join([x + '[^)]*\)' for x in svg_transforms]), t) + + for t in transforms: + op, arg = t.split('(') + op = op.strip() + # Keep only numbers + arg = [float(x) for x in re.findall(number_re, arg)] + print('transform: ' + op + ' '+ str(arg)) + + if op == 'matrix': + self.matrix *= Matrix(arg) + + if op == 'translate': + tx = arg[0] + if len(arg) == 1: ty = 0 + else: ty = arg[1] + self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) + + if op == 'scale': + sx = arg[0] + if len(arg) == 1: sy = sx + else: sy = arg[1] + self.matrix *= Matrix([sx, 0, 0, sy, 0, 0]) + + if op == 'rotate': + cosa = math.cos(math.radians(arg[0])) + sina = 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]) + if len(arg) != 1: + self.matrix *= Matrix([1, 0, 0, 1, -tx, -ty]) + + if op == 'skewX': + tana = math.tan(math.radians(arg[0])) + self.matrix *= Matrix([1, 0, tana, 1, 0, 0]) + + if op == 'skewY': + tana = math.tan(math.radians(arg[0])) + self.matrix *= Matrix([1, tana, 0, 1, 0, 0]) + + def transform(self, matrix=None): + if matrix is None: + matrix = self.matrix + else: + matrix *= self.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) + + def length(self, v, mode='xy'): + # Handle empty (non-existing) length element + if v is None: + return 0 + + # Get length value + m = re.search(number_re, v) + if m: value = m.group(0) + else: raise TypeError(v + 'is not a valid length') + + # Get length unit + m = re.search(unit_re, v) + if m: unit = m.group(0) + else: unit = None + + if unit == '%': + if mode == 'x': + return float(value) * unit_convert[unit] * self.viewport.x + if mode == 'y': + return float(value) * unit_convert[unit] * self.viewport.y + if mode == 'xy': + return float(value) * unit_convert[unit] * self.viewport.x # FIXME + + return float(value) * unit_convert[unit] + + def xlength(self, x): + return self.length(x, 'x') + def ylength(self, y): + return self.length(y, 'y') + + def flatten(self): + '''Flatten the SVG objects nested list into a flat (1-D) list, + removing Groups''' + # http://rightfootin.blogspot.fr/2006/09/more-on-python-flatten.html + # Assigning a slice a[i:i+1] with a list actually replaces the a[i] + # element with the content of the assigned list + i = 0 + flat = copy.deepcopy(self.items) + while i < len(flat): + while isinstance(flat[i], Group): + flat[i:i+1] = flat[i].items + 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 + # tag = 'svg' + + def __init__(self, filename=None): + Transformable.__init__(self) + if filename: + self.parse(filename) + + def parse(self, filename): + self.filename = filename + tree = etree.parse(filename) + self.root = tree.getroot() + if self.root.tag != svg_ns + 'svg': + 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() + self.items.append(top_group) + + # 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')) + sx = width / float(viewBox[2]) + sy = height / float(viewBox[3]) + tx = -float(viewBox[0]) + ty = -float(viewBox[1]) + top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) + + # Parse XML elements hierarchically with groups + top_group.append(self.root) + + self.transform() + + 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] + + def json(self): + return self.items + + +class Group(Transformable): + '''Handle svg elements''' + # class Group handles the tag + tag = 'g' + + def __init__(self, elt=None): + Transformable.__init__(self, elt) + + self.name = "" + if elt is not None: + + for id, value in elt.attrib.items(): + + id = self.parse_name( id ) + if id[ "name" ] == "label": + self.name = value + + @staticmethod + def parse_name( tag ): + m = re.match( r'({(.+)})?(.+)', tag ) + return { + 'namespace' : m.group( 2 ), + 'name' : m.group( 3 ), + } + + 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) + continue + # instanciate 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 + # self.transform(), so doing it here will result in the transformations + # being applied twice. + #item.matrix = self.matrix * item.matrix + item.viewport = self.viewport + + self.items.append(item) + # Recursively append if elt is a (group) + if elt.tag == svg_ns + 'g': + item.append(elt) + + def __repr__(self): + return ': ' + repr(self.items) + + def json(self): + return {'Group ' + self.id + " ({})".format( self.name ) : self.items} + +class Matrix: + ''' 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 ''' + + def __init__(self, vect=[1, 0, 0, 1, 0, 0]): + # Unit transformation vect by default + if len(vect) != 6: + raise ValueError("Bad vect size %d" % len(vect)) + self.vect = list(vect) + + def __mul__(self, other): + '''Matrix multiplication''' + if isinstance(other, Matrix): + a = self.vect[0] * other.vect[0] + self.vect[2] * other.vect[1] + b = self.vect[1] * other.vect[0] + self.vect[3] * other.vect[1] + c = self.vect[0] * other.vect[2] + self.vect[2] * other.vect[3] + d = self.vect[1] * other.vect[2] + self.vect[3] * other.vect[3] + e = self.vect[0] * other.vect[4] + self.vect[2] * other.vect[5] \ + + self.vect[4] + f = self.vect[1] * other.vect[4] + self.vect[3] * other.vect[5] \ + + self.vect[5] + return Matrix([a, b, c, d, e, f]) + + elif 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 + + def __str__(self): + return str(self.vect) + + def xlength(self, x): + return x * self.vect[0] + def ylength(self, y): + return y * self.vect[3] + + +COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' + +class Path(Transformable): + '''SVG ''' + # class Path handles the tag + tag = 'path' + + 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): + """Parse path string and build elements list""" + + pathlst = re.findall(number_re + r"|\ *[%s]\ *" % COMMANDS, pathstr) + + pathlst.reverse() + + command = None + current_pt = Point(0,0) + start_pt = None + + while pathlst: + if pathlst[-1].strip() in COMMANDS: + last_command = command + command = pathlst.pop().strip() + absolute = (command == command.upper()) + command = command.upper() + else: + if command is None: + raise ValueError("No command found at %d" % len(pathlst)) + + if command == 'M': + # MoveTo + x = pathlst.pop() + y = pathlst.pop() + pt = Point(x, y) + if absolute: + current_pt = pt + else: + current_pt += pt + start_pt = current_pt + + self.items.append(MoveTo(current_pt)) + + # MoveTo with multiple coordinates means LineTo + command = 'L' + + elif command == 'Z': + # Close Path + l = Segment(current_pt, start_pt) + self.items.append(l) + + + elif command in 'LHV': + # LineTo, Horizontal & Vertical line + # extra coord for H,V + if absolute: + x,y = current_pt.coord() + else: + x,y = (0,0) + + if command in 'LH': + x = pathlst.pop() + if command in 'LV': + y = pathlst.pop() + + pt = Point(x, y) + if not absolute: + pt += current_pt + + self.items.append(Segment(current_pt, pt)) + current_pt = pt + + elif command in 'CQ': + dimension = {'Q':3, 'C':4} + bezier_pts = [] + bezier_pts.append(current_pt) + for i in range(1,dimension[command]): + x = pathlst.pop() + y = pathlst.pop() + pt = Point(x, y) + if not absolute: + pt += current_pt + bezier_pts.append(pt) + + self.items.append(Bezier(bezier_pts)) + current_pt = pt + + elif command in 'TS': + # number of points to read + nbpts = {'T':1, 'S':2} + # the control point, from previous Bezier to mirror + ctrlpt = {'T':1, 'S':2} + # last command control + last = {'T': 'QT', 'S':'CS'} + + bezier_pts = [] + bezier_pts.append(current_pt) + + if last_command in last[command]: + pt0 = self.items[-1].control_point(ctrlpt[command]) + else: + pt0 = current_pt + pt1 = current_pt + # Symetrical of pt1 against pt0 + bezier_pts.append(pt1 + pt1 - pt0) + + for i in range(0,nbpts[command]): + x = pathlst.pop() + y = pathlst.pop() + pt = Point(x, y) + if not absolute: + pt += current_pt + bezier_pts.append(pt) + + self.items.append(Bezier(bezier_pts)) + current_pt = pt + + elif command == 'A': + rx = pathlst.pop() + ry = pathlst.pop() + xrot = pathlst.pop() + # Arc flags are not necesarily sepatated numbers + flags = pathlst.pop().strip() + large_arc_flag = flags[0] + if large_arc_flag not in '01': + print('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('Arc parsing failure') + 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])) +# self.items.append( +# Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) + + else: + pathlst.pop() + + def __str__(self): + return '\n'.join(str(x) for x in self.items) + + def __repr__(self): + return '' + + def segments(self, precision=0): + '''Return a list of segments, each segment is ended by a MoveTo. + A segment is a list of Points''' + ret = [] + # group items separated by MoveTo + for moveTo, group in itertools.groupby(self.items, + lambda x: isinstance(x, MoveTo)): + # Use only non MoveTo item + if not moveTo: + # Generate segments for each relevant item + seg = [x.segments(precision) for x in group] + # Merge all segments into one + ret.append(list(itertools.chain.from_iterable(seg))) + + return ret + + def simplify(self, precision): + '''Simplify segment with precision: + Remove any point which are ~aligned''' + ret = [] + for seg in self.segments(precision): + ret.append(simplify_segment(seg, precision)) + + return ret + +class Ellipse(Transformable): + '''SVG ''' + # class Ellipse handles the tag + tag = 'ellipse' + + 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'))) + self.rx = self.length(elt.get('rx')) + self.ry = self.length(elt.get('ry')) + self.style = elt.get('style') + + def __repr__(self): + return '' + + def bbox(self): + '''Bounding box''' + pmin = self.center - Point(self.rx, self.ry) + pmax = self.center + Point(self.rx, self.ry) + 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) + + 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) + y = self.center.y + self.ry * math.sin(2 * math.pi * t) + return Point(x,y) + + def segments(self, precision=0): + if max(self.rx, self.ry) < precision: + return [[self.center]] + + p = [(0,self.P(0)), (1, self.P(1))] + d = 2 * max(self.rx, self.ry) + + 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)) + + ret = [x for t,x in p] + return [ret] + + def simplify(self, precision): + return self + +# A circle is a special type of ellipse where rx = ry = radius +class Circle(Ellipse): + '''SVG ''' + # class Circle handles the tag + tag = 'circle' + + 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) + + def __repr__(self): + return '' + +class Rect(Transformable): + '''SVG ''' + # class Rect handles the tag + tag = 'rect' + + 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'))) + + self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), + self.P1.y + self.ylength(elt.get('height'))) + + def __repr__(self): + return '' + + def bbox(self): + '''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): + self.P1 = self.matrix * self.P1 + self.P2 = self.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) + + ret.append([self.P1, Pa, self.P2, Pb, self.P1]) + return ret + + def simplify(self, precision): + return self.segments(precision) + +class Line(Transformable): + '''SVG ''' + # class Line handles the tag + tag = 'line' + + 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'))) + self.P2 = Point(self.xlength(elt.get('x2')), + self.ylength(elt.get('y2'))) + self.segment = Segment(self.P1, self.P2) + + def __repr__(self): + return '' + + def bbox(self): + '''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): + self.P1 = self.matrix * self.P1 + self.P2 = self.matrix * self.P2 + self.segment = Segment(self.P1, self.P2) + + def segments(self, precision=0): + return [self.segment.segments()] + + def simplify(self, precision): + return self.segments(precision) + +# overwrite JSONEncoder for svg classes which have defined a .json() method +class JSONEncoder(json.JSONEncoder): + def default(self, obj): + if not isinstance(obj, tuple(svgClass.values() + [Svg])): + return json.JSONEncoder.default(self, obj) + + if not hasattr(obj, 'json'): + return repr(obj) + + return obj.json() + +## Code executed on module load ## + +# 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): + tag = getattr(cls, 'tag', None) + if tag: + svgClass[svg_ns + tag] = cls + diff --git a/pcb/svg2mod/svg2mod.py b/pcb/svg2mod/svg2mod.py new file mode 100644 index 0000000..2067c62 --- /dev/null +++ b/pcb/svg2mod/svg2mod.py @@ -0,0 +1,1465 @@ +#!/usr/bin/python + +from __future__ import absolute_import + +import argparse +import datetime +import os +from pprint import pformat, pprint +import re +import svg +import sys + + +#---------------------------------------------------------------------------- +DEFAULT_DPI = 96 # 96 as of Inkscape 0.92 + +def main(): + + args, parser = get_arguments() + + pretty = args.format == 'pretty' + use_mm = args.units == 'mm' + + if pretty: + + if not use_mm: + print( "Error: decimil 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 + ) + + # 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.scale_factor, + args.precision, + args.dpi, + ) + + 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.scale_factor, + args.precision, + args.dpi, + include_reverse = not args.front_only, + ) + + 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.scale_factor, + args.precision, + use_mm = use_mm, + dpi = args.dpi, + include_reverse = not args.front_only, + ) + + # Export the footprint: + exported.write() + + +#---------------------------------------------------------------------------- + +class LineSegment( object ): + + #------------------------------------------------------------------------ + + @staticmethod + def _on_segment( p, q, r ): + """ Given three colinear points p, q, and r, check if + point q lies on line segment pr. """ + + if ( + q.x <= max( p.x, r.x ) and + q.x >= min( p.x, r.x ) and + q.y <= max( p.y, r.y ) and + q.y >= min( p.y, r.y ) + ): + return True + + return False + + + #------------------------------------------------------------------------ + + @staticmethod + def _orientation( p, q, r ): + """ Find orientation of ordered triplet (p, q, r). + Returns following values + 0 --> p, q and r are colinear + 1 --> Clockwise + 2 --> Counterclockwise + """ + + val = ( + ( q.y - p.y ) * ( r.x - q.x ) - + ( q.x - p.x ) * ( r.y - q.y ) + ) + + if val == 0: return 0 + if val > 0: return 1 + return 2 + + + #------------------------------------------------------------------------ + + def __init__( self, p = None, q = None ): + + self.p = p + self.q = q + + + #------------------------------------------------------------------------ + + def connects( self, 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 + return False + + + #------------------------------------------------------------------------ + + def intersects( self, segment ): + """ Return true if line segments 'p1q1' and 'p2q2' intersect. + Adapted from: + http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/ + """ + + # Find the four orientations needed for general and special cases: + o1 = self._orientation( self.p, self.q, segment.p ) + o2 = self._orientation( self.p, self.q, segment.q ) + o3 = self._orientation( segment.p, segment.q, self.p ) + o4 = self._orientation( segment.p, segment.q, self.q ) + + return ( + + # General case: + ( o1 != o2 and o3 != o4 ) + + or + + # p1, q1 and p2 are colinear 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: + ( 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: + ( 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: + ( o4 == 0 and self._on_segment( segment.p, self.q, segment.q ) ) + ) + + + #------------------------------------------------------------------------ + + def q_next( self, q ): + + self.p = self.q + self.q = q + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +class PolygonSegment( object ): + + #------------------------------------------------------------------------ + + def __init__( self, points ): + + self.points = points + + if len( points ) < 3: + print( + "Warning:" + " Path segment has only {} points (not a polygon?)".format( + len( 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. + def _find_insertion_point( self, hole, holes ): + + #print( " Finding insertion point. {} holes".format( len( holes ) ) ) + + # 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 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( + bridge, + check_connects = ( + other_hole == hole or other_hole == self + ) + ): break + + #print( " Hole does not intersect." ) + + else: + 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" + ) + + + #------------------------------------------------------------------------ + + # 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 ): + + points = self.points + + if index > 0: + + # Strip off end point, which is a duplicate of the start point: + points = points[ : -1 ] + + points = points[ index : ] + points[ : index ] + + points.append( + svg.Point( points[ 0 ].x, points[ 0 ].y ) + ) + + return points + + + #------------------------------------------------------------------------ + + # Return a list of points with the given polygon segments (paths) inlined. + def inline( self, segments ): + + if len( segments ) < 1: + return self.points + + print( " Inlining {} segments...".format( len( segments ) ) ) + + all_segments = segments[ : ] + [ self ] + insertions = [] + + # Find the insertion point for each hole: + for hole in segments: + + insertion = self._find_insertion_point( + hole, all_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: + + while ip <= insertion[ 0 ]: + inlined.append( points[ ip ] ) + ip += 1 + + if ( + inlined[ -1 ].x == insertion[ 1 ][ 0 ].x and + inlined[ -1 ].y == insertion[ 1 ][ 0 ].y + ): + inlined += insertion[ 1 ][ 1 : -1 ] + else: + inlined += insertion[ 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 + + + #------------------------------------------------------------------------ + + def intersects( self, line_segment, check_connects ): + + hole_segment = LineSegment() + + # Check each segment of other hole for intersection: + for point in self.points: + + hole_segment.q_next( point ) + + if hole_segment.p is not None: + + if ( + check_connects and + line_segment.connects( hole_segment ) + ): continue + + if line_segment.intersects( hole_segment ): + + #print( "Intersection detected." ) + + return True + + return False + + + #------------------------------------------------------------------------ + + # Apply all transformations and rounding, then remove duplicate + # consecutive points along the path. + def process( self, transformer, flip ): + + points = [] + for point in self.points: + + point = transformer.transform_point( point, flip ) + + if ( + len( points ) < 1 or + point.x != points[ -1 ].x or + point.y != points[ -1 ].y + ): + points.append( point ) + + if ( + 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, + #) ) + + points.append( svg.Point( + points[ 0 ].x, + 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 + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +class Svg2ModImport( object ): + + #------------------------------------------------------------------------ + + def __init__( self, file_name, module_name, module_value ): + + self.file_name = file_name + self.module_name = module_name + self.module_value = module_value + + print( "Parsing SVG..." ) + self.svg = svg.parse( file_name ) + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +class Svg2ModExport( object ): + + #------------------------------------------------------------------------ + + @staticmethod + def _convert_decimil_to_mm( decimil ): + return float( decimil ) * 0.00254 + + + #------------------------------------------------------------------------ + + @staticmethod + def _convert_mm_to_decimil( 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 property in item.style.split( ";" ): + + nv = property.split( ":" ); + name = nv[ 0 ].strip() + value = nv[ 1 ].strip() + + if name == "fill" and value == "none": + fill = False + + elif name == "stroke" and value == "none": + stroke = False + + elif name == "stroke-width": + value = value.replace( "px", "" ) + stroke_width = float( value ) * 25.4 / float(self.dpi) + + if not stroke: + stroke_width = 0.0 + elif stroke_width is None: + # Give a default stroke width? + stroke_width = self._convert_decimil_to_mm( 1 ) + + return fill, stroke, stroke_width + + + #------------------------------------------------------------------------ + + def __init__( + self, + svg2mod_import, + file_name, + scale_factor = 1.0, + precision = 20.0, + use_mm = True, + dpi = DEFAULT_DPI, + ): + if use_mm: + # 25.4 mm/in; + scale_factor *= 25.4 / float(dpi) + use_mm = True + else: + # PCBNew uses "decimil" (10K DPI); + scale_factor *= 10000.0 / float(dpi) + + self.imported = svg2mod_import + self.file_name = file_name + self.scale_factor = scale_factor + self.precision = precision + self.use_mm = use_mm + self.dpi = dpi + + #------------------------------------------------------------------------ + + 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 + + self.translation = svg.Point( + 0.0 - adjust_x, + 0.0 - adjust_y, + ) + + + #------------------------------------------------------------------------ + + # Find and keep only the layers of interest. + def _prune( self, items = None ): + + 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: + print( "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 + + elif isinstance( item, svg.Path ): + + segments = [ + PolygonSegment( segment ) + for segment in item.segments( + precision = self.precision + ) + ] + + for segment in segments: + segment.process( self, flip ) + + if len( segments ) > 1: + points = segments[ 0 ].inline( segments[ 1 : ] ) + + 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 + ) + + print( " Writing polygon with {} points".format( + len( points ) ) + ) + + self._write_polygon( + points, layer, fill, stroke, stroke_width + ) + + else: + print( "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_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 ) + 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 ): + + 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 ): + + self._prune() + + # Must come after pruning: + translation = self._calculate_translation() + + print( "Writing module file: {}".format( self.file_name ) ) + self.output_file = open( self.file_name, 'w' ) + + self._write_library_intro() + + self._write_modules() + + self.output_file.close() + self.output_file = None + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +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 ], + '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, + scale_factor = 1.0, + precision = 20.0, + use_mm = True, + dpi = DEFAULT_DPI, + include_reverse = True, + ): + super( Svg2ModExportLegacy, self ).__init__( + svg2mod_import, + file_name, + scale_factor, + precision, + use_mm, + dpi, + ) + + self.include_reverse = include_reverse + + + #------------------------------------------------------------------------ + + 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 ): + + 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 +# +# {3} +# +""".format( + datetime.datetime.now().strftime( "%a %d %b %Y %I:%M:%S %p %Z" ), + units, + modules_list, + self.imported.file_name, +) + ) + + + #------------------------------------------------------------------------ + + 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_decimil_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 ): + + #------------------------------------------------------------------------ + + def __init__( + self, + svg2mod_import, + file_name, + 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, + scale_factor, + precision, + use_mm, + dpi, + include_reverse, + ) + + + #------------------------------------------------------------------------ + + def _parse_output_file( self ): + + print( "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( "Units[\s]+mm[\s]*", line ) + if m is not None: + print( " Use mm detected" ) + 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 ) + ) + + #print( "Pre-index:" ) + #pprint( self.pre_index ) + + #print( "Post-index:" ) + #pprint( self.post_index ) + + #print( "Loaded modules:" ) + #pprint( self.loaded_modules ) + + 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 ) + + print( " 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 ): + + # 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 ): + + 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 ): + + if front: + return self.layer_map[ name ].format("F") + else: + return self.layer_map[ name ].format("B") + + + #------------------------------------------------------------------------ + + def _get_module_name( self, front = None ): + + return self.imported.module_name + + + #------------------------------------------------------------------------ + + def _write_library_intro( self ): + + self.output_file.write( """(module {0} (layer F.Cu) (tedit {1:8X}) + (attr smd) + (descr "{2}") + (tags {3}) +""".format( + self.imported.module_name, #0 + int( round( os.path.getctime( #1 + self.imported.file_name + ) ) ), + "Imported from {}".format( self.imported.file_name ), #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( 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 ): + + self.output_file.write( + " )\n (layer {})\n (width {})\n )".format( + layer, stroke_width + ) + ) + + + #------------------------------------------------------------------------ + + def _write_polygon_header( self, points, layer ): + + self.output_file.write( "\n (fp_poly\n (pts \n" ) + + + #------------------------------------------------------------------------ + + def _write_polygon_point( self, point ): + + 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(): + + parser = argparse.ArgumentParser( + description = ( + 'Convert Inkscape SVG drawings to KiCad footprint modules.' + ) + ) + + #------------------------------------------------------------------------ + + parser.add_argument( + '-i', '--input-file', + type = str, + dest = 'input_file_name', + metavar = 'FILENAME', + help = "name of the SVG file", + required = True, + ) + + parser.add_argument( + '-o', '--output-file', + type = str, + dest = 'output_file_name', + metavar = 'FILENAME', + help = "name of the module file", + ) + + parser.add_argument( + '--name', '--module-name', + type = str, + dest = 'module_name', + metavar = 'NAME', + help = "base name of the module", + default = "svg2mod", + ) + + parser.add_argument( + '--value', '--module-value', + type = str, + dest = 'module_value', + metavar = 'VALUE', + help = "value of the module", + default = "G***", + ) + + 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 (float)", + 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, + dest = 'format', + metavar = 'FORMAT', + choices = [ 'legacy', 'pretty' ], + help = "output module file format (legacy|pretty)", + default = 'pretty', + ) + + parser.add_argument( + '--units', + type = str, + dest = 'units', + metavar = 'UNITS', + choices = [ 'decimil', 'mm' ], + help = "output units, if output format is legacy (decimil|mm)", + default = 'mm', + ) + + parser.add_argument( + '-d', '--dpi', + type = int, + dest = 'dpi', + metavar = 'DPI', + help = "DPI of the SVG file (int)", + default = DEFAULT_DPI, + ) + + return parser.parse_args(), parser + + + #------------------------------------------------------------------------ + +#---------------------------------------------------------------------------- + +main() + + +#---------------------------------------------------------------------------- +# vi: set et sts=4 sw=4 ts=4: