begin of PCB footprint parser
Stefan Schuermans

Stefan Schuermans commited on 2021-02-17 19:13:39
Showing 1 changed files, with 343 additions and 0 deletions.

... ...
@@ -0,0 +1,343 @@
1
+#! /usr/bin/env python3
2
+
3
+import ezdxf
4
+import re
5
+
6
+
7
+class StringBuffer:
8
+    """
9
+    Store a string, an index and provide some operations.
10
+    """
11
+
12
+    def __init__(self, string: str):
13
+        self._string = string
14
+        self._index = 0
15
+        self._stack = []
16
+
17
+    def __enter__(self):
18
+        self.enter()
19
+        return self
20
+
21
+    def __exit__(self, exc_type, _exc_value, _traceback):
22
+        if exc_type is None:
23
+            self.complete()
24
+        else:
25
+            self.backtrack()
26
+
27
+    def advance(self, length: int):
28
+        """
29
+        Advance the index by length.
30
+        """
31
+        self._index = min(len(self._string), self._index + length)
32
+
33
+    def backtrack(self):
34
+        """
35
+        Track back from parsing something.
36
+        Pop index from stack and use it as the new current index.
37
+        """
38
+        self._index = self._stack[-1]
39
+        del self._stack[-1]
40
+
41
+    def check(self, s: str) -> bool:
42
+        """
43
+        Check if string s follows index.
44
+        If so, advance by length of s and return True
45
+        Otherwise, keep index and return False.
46
+        """
47
+        if self.peek(len(s)) == s:
48
+            self.advance(len(s))
49
+            return True
50
+        return False
51
+
52
+    def complete(self):
53
+        """
54
+        Complete parsing something.
55
+        Pop index from stack and throw it away.
56
+        """
57
+        del self._stack[-1]
58
+
59
+    def enter(self):
60
+        """
61
+        Enter parsing something.
62
+        Push current index onto stack.
63
+        """
64
+        self._stack.append(self._index)
65
+
66
+    def expect(self, s: str):
67
+        """
68
+        Check that string s follows index.
69
+        If so, advance.
70
+        If not, throw ValueError.
71
+        """
72
+        if not self.check(s):
73
+            raise ValueError
74
+
75
+    def get(self, length: int) -> str:
76
+        """
77
+        Return the next length characters and advance the index.
78
+        """
79
+        s = self.peek(length)
80
+        self.advance(length)
81
+        return s
82
+
83
+    def getRe(self, ptrn: re.Pattern):
84
+        """
85
+        Return the matching the regular expression pattern and advance
86
+        index or throw ValueError.
87
+        """
88
+        m = ptrn.match(self._string[self._index:])
89
+        if not m:
90
+            raise ValueError()
91
+        self._index += len(m.group(0))
92
+        return m
93
+
94
+    def peek(self, length: int) -> str:
95
+        """
96
+        Return the next length characters without advancing the index.
97
+        """
98
+        return self._string[self._index:self._index + length]
99
+
100
+
101
+class PcbBaseParser(StringBuffer):
102
+    """
103
+    Parser for the basic element of GNU PCB files.
104
+    """
105
+
106
+    re_float = re.compile(r'^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)'
107
+                          r'(?:[]eE[+-]?[0-9]\+)?')
108
+    re_int = re.compile(r'^[+-]?[0-9]+')
109
+    re_quoted = re.compile(r'^"([^"]*)"')
110
+    re_space = re.compile(r'^\s+', re.MULTILINE)
111
+    re_space_opt = re.compile(r'^\s*', re.MULTILINE)
112
+
113
+    def parseCoord(self) -> float:
114
+        """
115
+        Parse a coordinate.
116
+        """
117
+        with self:
118
+            f = self.parseFloat()
119
+            if self.check('mil'):
120
+                f *= 100  # native unit == .01mil
121
+            elif self.check('mm'):
122
+                f *= 1e5/25.4  # 25.4mm == 1inch == 1e5 * .01mil
123
+            return f
124
+
125
+    def parseFloat(self) -> float:
126
+        """
127
+        Parse a floating point value.
128
+        """
129
+        with self:
130
+            return float(self.getRe(self.re_float).group(0))
131
+
132
+    def parseInt(self) -> int:
133
+        """
134
+        Parse a decimal integer.
135
+        """
136
+        with self:
137
+            return int(self.getRe(self.re_int).group(0))
138
+
139
+    def parseQuoted(self):
140
+        """
141
+        Parse quoted string.
142
+        """
143
+        with self:
144
+            return self.getRe(self.re_quoted).group(1)
145
+
146
+    def parseSpace(self):
147
+        """
148
+        Parse at least one whitespace character or more.
149
+        """
150
+        with self:
151
+            self.getRe(self.re_space)
152
+
153
+    def parseSpaceOpt(self):
154
+        """
155
+        Parse any number of whitespace characters.
156
+        """
157
+        with self:
158
+            self.getRe(self.re_space_opt)
159
+
160
+
161
+class PcbFootprintParser(PcbBaseParser):
162
+    """
163
+    Parser for a GNU PCB footprint file.
164
+    """
165
+
166
+    def parseBody(self, contents: list):
167
+        """
168
+        Parse a body of a block.
169
+        contents: list of lambdas with options for recursive descent
170
+        """
171
+        with self:
172
+            self.parseOpen()
173
+            while True:
174
+                # end of body
175
+                try:
176
+                    self.parseClose()
177
+                    break
178
+                except ValueError:
179
+                    pass
180
+                # try options
181
+                for c in contents:
182
+                    try:
183
+                        c()
184
+                        break  # option succeded, go to next content
185
+                    except ValueError:
186
+                        pass  # an option failed, try next option
187
+                # all options failed
188
+                else:
189
+                    raise ValueError()
190
+
191
+    def parseClose(self):
192
+        """
193
+        Parse a closing parenthesis.
194
+        """
195
+        with self:
196
+            self.parseSpaceOpt()
197
+            self.expect(')')
198
+
199
+    def parseElementBlock(self):
200
+        """
201
+        Parse a GNU PCB element block.
202
+        """
203
+        with self:
204
+            self.parseElementClause()
205
+            self.parseBody([
206
+                lambda: self.parseElementLineClause(),
207
+                lambda: self.parsePadClause()
208
+            ])
209
+
210
+
211
+    def parseElementClause(self):
212
+        """
213
+        Parse a GNU PCB element clause.
214
+        """
215
+        with self:
216
+            self.parseSpaceOpt()
217
+            self.expect('Element')
218
+            self.parseSpaceOpt()
219
+            self.expect('[')
220
+            self.parseSpaceOpt()
221
+            s_flags = self.parseQuoted()
222
+            self.parseSpace()
223
+            desc = self.parseQuoted()
224
+            self.parseSpace()
225
+            name = self.parseQuoted()
226
+            self.parseSpace()
227
+            value = self.parseQuoted()
228
+            self.parseSpace()
229
+            m_x = self.parseCoord()
230
+            self.parseSpace()
231
+            m_y = self.parseCoord()
232
+            self.parseSpace()
233
+            t_x = self.parseCoord()
234
+            self.parseSpace()
235
+            t_y = self.parseCoord()
236
+            self.parseSpace()
237
+            t_dir = self.parseInt()
238
+            self.parseSpace()
239
+            t_scale = self.parseInt()
240
+            self.parseSpace()
241
+            t_s_flags = self.parseQuoted()
242
+            self.parseSpaceOpt()
243
+            self.expect(']')
244
+
245
+    def parseElementLineClause(self):
246
+        """
247
+        Parse a GNU PCB element line clause.
248
+        """
249
+        with self:
250
+            self.parseSpaceOpt()
251
+            self.expect('ElementLine')
252
+            self.parseSpaceOpt()
253
+            self.expect('[')
254
+            self.parseSpaceOpt()
255
+            r_x1 = self.parseCoord()
256
+            self.parseSpace()
257
+            r_y1 = self.parseCoord()
258
+            self.parseSpace()
259
+            r_x2 = self.parseCoord()
260
+            self.parseSpace()
261
+            r_y2 = self.parseCoord()
262
+            self.parseSpace()
263
+            thickness = self.parseCoord()
264
+            self.parseSpaceOpt()
265
+            self.expect(']')
266
+
267
+    def parseOpen(self):
268
+        """
269
+        Parse an opening parenthesis.
270
+        """
271
+        with self:
272
+            self.parseSpaceOpt()
273
+            self.expect('(')
274
+
275
+    def parsePadClause(self):
276
+        """
277
+        Parse a GNU PCB pad clause.
278
+        """
279
+        with self:
280
+            self.parseSpaceOpt()
281
+            self.expect('Pad')
282
+            self.parseSpaceOpt()
283
+            self.expect('[')
284
+            self.parseSpaceOpt()
285
+            r_x1 = self.parseCoord()
286
+            self.parseSpace()
287
+            r_y1 = self.parseCoord()
288
+            self.parseSpace()
289
+            r_x2 = self.parseCoord()
290
+            self.parseSpace()
291
+            r_y2 = self.parseCoord()
292
+            self.parseSpace()
293
+            thickness = self.parseCoord()
294
+            self.parseSpace()
295
+            clearance = self.parseCoord()
296
+            self.parseSpace()
297
+            mask = self.parseCoord()
298
+            self.parseSpace()
299
+            name = self.parseQuoted()
300
+            self.parseSpace()
301
+            number = self.parseQuoted()
302
+            self.parseSpace()
303
+            s_flags = self.parseQuoted()
304
+            self.parseSpaceOpt()
305
+            self.expect(']')
306
+
307
+
308
+class PcbFootprint:
309
+    """
310
+    A footprint of GNU PCB.
311
+    """
312
+
313
+    def __init__(self):
314
+        self.reset()
315
+
316
+    def readPcb(self, file_name: str):
317
+        """
318
+        Read a PCB footprint file.
319
+        """
320
+        self.reset()
321
+        with open(file_name, 'r') as f:
322
+            s = f.read()
323
+        p = PcbFootprintParser(s)
324
+        p.parseElementBlock()
325
+
326
+    def reset(self):
327
+        pass
328
+
329
+
330
+def write_dxf(file_name: str):
331
+    doc = ezdxf.new('R12')
332
+    msp = doc.modelspace()
333
+    msp.add_circle((1, 2), radius=3)
334
+    doc.saveas(file_name)
335
+
336
+
337
+def main():
338
+    fp = PcbFootprint()
339
+    fp.readPcb('../pcb_footpr/SMD_1206')
340
+
341
+
342
+if __name__ == '__main__':
343
+    main()
0 344