#! /usr/bin/env python3 import ezdxf import re import sys class ParseError(Exception): def __init__(self, msg: str, pos: str): super().__init__(self, msg, pos) self.msg = msg self.pos = pos def __repr__(self) -> str: return f'ParseError({repr(self.msg):s}, {repr(self.pos):s})' def __str__(self) -> str: return f'ParseError: {self.msg:s} at {self.pos:s}' class StringBuffer: """ Store a string, an index and provide some operations. """ def __init__(self, string: str): self._string = string self._index = 0 self._stack = [] def __enter__(self): self.enter() return self def __exit__(self, exc_type, _exc_value, _traceback): if exc_type is None: self.complete() else: self.backtrack() def advance(self, length: int): """ Advance the index by length. """ self._index = min(len(self._string), self._index + length) def backtrack(self): """ Track back from parsing something. Pop index from stack and use it as the new current index. """ self._index = self._stack[-1] del self._stack[-1] def check(self, s: str) -> bool: """ Check if string s follows index. If so, advance by length of s and return True Otherwise, keep index and return False. """ if self.peek(len(s)) == s: self.advance(len(s)) return True return False def complete(self): """ Complete parsing something. Pop index from stack and throw it away. """ del self._stack[-1] def enter(self): """ Enter parsing something. Push current index onto stack. """ self._stack.append(self._index) def expect(self, s: str): """ Check that string s follows index. If so, advance. If not, throw ParseError. """ if not self.check(s): raise ParseError(f'expected {repr(s):s}', self.pos()) def get(self, length: int) -> str: """ Return the next length characters and advance the index. """ s = self.peek(length) self.advance(length) return s def getRe(self, ptrn: re.Pattern): """ Return the matching the regular expression pattern and advance index or throw ParseError. """ m = ptrn.match(self._string[self._index:]) if not m: raise ParseError(f'expected pattern {repr(ptrn.pattern):s}', self.pos()) self._index += len(m.group(0)) return m def peek(self, length: int) -> str: """ Return the next length characters without advancing the index. """ return self._string[self._index:self._index + length] def pos(self) -> str: """ Get string describing current position. """ before = self._string[:self._index].split('\n') after = self._string[self._index:].split('\n') line_no = len(before) column_no = len(before[-1]) + 1 rest = after[0] return f'line {line_no:d} column {column_no:d} {repr(rest):s}' class PcbBaseParser(StringBuffer): """ Parser for the basic element of GNU PCB files. """ re_float = re.compile(r'^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)' r'(?:[]eE[+-]?[0-9]\+)?') re_int = re.compile(r'^[+-]?[0-9]+') re_quoted = re.compile(r'^"([^"]*)"') re_space = re.compile(r'^\s+', re.MULTILINE) re_space_opt = re.compile(r'^\s*', re.MULTILINE) known_string_flags = set([ 'edge2', 'hole', 'nopaste', 'octagon', 'onsolder', 'pin', 'showname', 'square', 'via' ]) def parseCoord(self) -> float: """ Parse a coordinate. """ with self: f = self.parseFloat() if self.check('mil'): f *= 100 # native unit == .01mil elif self.check('mm'): f *= 1e5 / 25.4 # 25.4mm == 1inch == 1e5 * .01mil return f def parseFloat(self) -> float: """ Parse a floating point value. """ with self: s = self.getRe(self.re_float).group(0) try: return float(s) except ValueError as exc: raise ParseError(f'invalid floating point value {repr(s):s}', self.pos()) def parseInt(self) -> int: """ Parse a decimal integer. """ with self: s = self.getRe(self.re_int).group(0) try: return int(s) except ValueError as exc: raise ParseError('invalid integer value {repr(s):s}', self.pos()) def parseQuoted(self) -> str: """ Parse a quoted string. """ with self: return self.getRe(self.re_quoted).group(1) def parseStringFlags(self) -> list: """ Parse string flags. """ with self: q = self.parseQuoted() fs = set(q.split(',')) fs -= {''} unk = fs - self.known_string_flags if unk: unknown = ','.join(sorted(unk)) raise ParseError(f'unknown string flags {unknown:s}', self.pos()) return sorted(fs) def parseSpace(self): """ Parse at least one whitespace character or more. """ with self: self.getRe(self.re_space) def parseSpaceOpt(self): """ Parse any number of whitespace characters. """ with self: self.getRe(self.re_space_opt) class PcbFootprintParser(PcbBaseParser): """ Parser for a GNU PCB footprint file. """ def parseBody(self, contents: list): """ Parse a body of a block. contents: list of lambdas with options for recursive descent """ with self: self.parseOpen() while True: excs = [] # collect parse errors # end of body try: self.parseClose() break except ParseError as exc: excs.append(exc) # not end of body, try options # try options for c in contents: try: c() # option succeded, go to next content break except ParseError as exc: # an option failed, try next option excs.append(exc) # all options failed else: raise ParseError( 'invalid syntax, all options failed: ' + ', '.join([exc.msg for exc in excs]), self.pos()) def parseClose(self): """ Parse a closing parenthesis. """ with self: self.parseSpaceOpt() self.expect(')') self.parseSpaceOpt() def parseElementArcClause(self): """ Parse a GNU PCB element arc clause. """ with self: self.parseSpaceOpt() self.expect('ElementArc') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() r_x = self.parseCoord() self.parseSpace() r_y = self.parseCoord() self.parseSpace() width = self.parseCoord() self.parseSpace() height = self.parseCoord() self.parseSpace() start_angle = self.parseFloat() self.parseSpace() end_angle = self.parseFloat() self.parseSpace() thickness = self.parseCoord() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() def parseElementBlock(self): """ Parse a GNU PCB element block. """ with self: self.parseElementClause() self.parseBody([ # parse options lambda: self.parseElementArcClause(), lambda: self.parseElementLineClause(), lambda: self.parsePadClause(), lambda: self.parsePinClause() ]) def parseElementClause(self): """ Parse a GNU PCB element clause. """ with self: self.parseSpaceOpt() self.expect('Element') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() s_flags = self.parseStringFlags() self.parseSpace() desc = self.parseQuoted() self.parseSpace() name = self.parseQuoted() self.parseSpace() value = self.parseQuoted() self.parseSpace() m_x = self.parseCoord() self.parseSpace() m_y = self.parseCoord() self.parseSpace() t_x = self.parseCoord() self.parseSpace() t_y = self.parseCoord() self.parseSpace() t_dir = self.parseInt() self.parseSpace() t_scale = self.parseInt() self.parseSpace() t_s_flags = self.parseStringFlags() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() def parseElementLineClause(self): """ Parse a GNU PCB element line clause. """ with self: self.parseSpaceOpt() self.expect('ElementLine') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() r_x1 = self.parseCoord() self.parseSpace() r_y1 = self.parseCoord() self.parseSpace() r_x2 = self.parseCoord() self.parseSpace() r_y2 = self.parseCoord() self.parseSpace() thickness = self.parseCoord() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() def parseOpen(self): """ Parse an opening parenthesis. """ with self: self.parseSpaceOpt() self.expect('(') self.parseSpaceOpt() def parsePadClause(self): """ Parse a GNU PCB pad clause. """ with self: self.parseSpaceOpt() self.expect('Pad') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() r_x1 = self.parseCoord() self.parseSpace() r_y1 = self.parseCoord() self.parseSpace() r_x2 = self.parseCoord() self.parseSpace() r_y2 = self.parseCoord() self.parseSpace() thickness = self.parseCoord() self.parseSpace() clearance = self.parseCoord() self.parseSpace() mask = self.parseCoord() self.parseSpace() name = self.parseQuoted() self.parseSpace() number = self.parseQuoted() self.parseSpace() s_flags = self.parseStringFlags() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() def parsePinClause(self): """ Parse a GNU PCB pin clause. """ with self: self.parseSpaceOpt() self.expect('Pin') self.parseSpaceOpt() self.expect('[') self.parseSpaceOpt() r_x = self.parseCoord() self.parseSpace() r_y = self.parseCoord() self.parseSpace() thickness = self.parseCoord() self.parseSpace() clearance = self.parseCoord() self.parseSpace() mask = self.parseCoord() self.parseSpace() drill = self.parseCoord() self.parseSpace() name = self.parseQuoted() self.parseSpace() number = self.parseQuoted() self.parseSpace() s_flags = self.parseStringFlags() self.parseSpaceOpt() self.expect(']') self.parseSpaceOpt() class PcbFootprint: """ A footprint of GNU PCB. """ def __init__(self): self.reset() def readPcb(self, file_name: str): """ Read a PCB footprint file. """ self.reset() with open(file_name, 'r') as f: s = f.read() p = PcbFootprintParser(s) p.parseElementBlock() def reset(self): pass def write_dxf(file_name: str): doc = ezdxf.new('R12') msp = doc.modelspace() msp.add_circle((1, 2), radius=3) doc.saveas(file_name) def main(): fp = PcbFootprint() fp.readPcb(sys.argv[1]) if __name__ == '__main__': main()