From b3e0ceb5c3ec755b09d2f005b8e3dcbed22d45a1 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 20 Feb 2015 22:24:34 -0500 Subject: Add IPC-D-356 Netlist Parsing --- gerber/ipc356.py | 314 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 gerber/ipc356.py (limited to 'gerber/ipc356.py') diff --git a/gerber/ipc356.py b/gerber/ipc356.py new file mode 100644 index 0000000..2b6f1f6 --- /dev/null +++ b/gerber/ipc356.py @@ -0,0 +1,314 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# Modified from parser.py by Paulo Henrique Silva +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +import re +from .cam import FileSettings + +# Net Name Variables +_NNAME = re.compile(r'^NNAME\d+$') + +# Board Edge Coordinates +_COORD = re.compile(r'X?(?P[\d\s]*)?Y?(?P[\d\s]*)?') + + +def read(filename): + """ Read data from filename and return an IPC_D_356 + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.ipc356.IPC_D_356` + An IPC_D_356 object created from the specified file. + + """ + # File object should use settings from source file by default. + return IPC_D_356.from_file(filename) + + +class IPC_D_356(object): + + @classmethod + def from_file(self, filename): + p = IPC_D_356_Parser() + return p.parse(filename) + + + def __init__(self, statements, settings): + self.statements = statements + self.units = settings.units + self.angle_units = settings.angle_units + + @property + def settings(self): + return FileSettings(units=self.units, angle_units=self.angle_units) + + @property + def comments(self): + return [record for record in self.statements + if isinstance(record, IPC356_Comment)] + + @property + def parameters(self): + return [record for record in self.statements + if isinstance(record, IPC356_Parameter)] + + @property + def test_records(self): + return [record for record in self.statements + if isinstance(record, IPC356_TestRecord)] + + @property + def nets(self): + return list(set([rec.net_name for rec in self.test_records + if rec.net_name is not None])) + + @property + def components(self): + return list(set([rec.id for rec in self.test_records + if rec.id is not None and rec.id != 'VIA'])) + + @property + def vias(self): + return [rec.id for rec in self.test_records if rec.id == 'VIA'] + + @property + def board_outline(self): + outline = [stmt for stmt in self.statements if isinstance(stmt, IPC356_BoardEdge)] + if len(outline): + return outline[0].points + else: + return None + +class IPC_D_356_Parser(object): + # TODO: Allow multi-line statements (e.g. Altium board edge) + def __init__(self): + self.units = 'inch' + self.angle_units = 'degrees' + self.statements = [] + self.nnames = {} + + @property + def settings(self): + return FileSettings(units=self.units, angle_units=self.angle_units) + + def parse(self, filename): + with open(filename, 'r') as f: + for line in f: + + if line[0] == 'C': + # Comment + self.statements.append(IPC356_Comment.from_line(line)) + + elif line[0] == 'P': + # Parameter + p = IPC356_Parameter.from_line(line) + if p.parameter == 'UNITS': + if p.value in ('CUST', 'CUST 0'): + self.units = 'inch' + self.angle_units = 'degrees' + elif p.value == 'CUST 1': + self.units = 'metric' + self.angle_units = 'degrees' + elif p.value == 'CUST 2': + self.units = 'inch' + self.angle_units = 'radians' + self.statements.append(p) + if _NNAME.match(p.parameter): + # Add to list of net name variables + self.nnames[p.parameter] = p.value + + elif line[0] == '3' and line[2] == '7': + # Test Record + record = IPC356_TestRecord.from_line(line, self.settings) + + # Substitute net name variables + net = record.net_name + if (_NNAME.match(net) and net in self.nnames.keys()): + record.net_name = self.nnames[record.net_name] + self.statements.append(record) + + elif line[0:3] == '389': + # Altium Board Edge Info + self.statements.append(IPC356_BoardEdge.from_line(line, self.settings)) + + elif line[0] == '9': + self.multiline = False + self.statements.append(IPC356_EndOfFile()) + + return IPC_D_356(self.statements, self.settings) + + +class IPC356_Comment(object): + @classmethod + def from_line(cls, line): + if line[0] != 'C': + raise ValueError('Not a valid comment statment') + comment = line[2:].strip() + return cls(comment) + + def __init__(self, comment): + self.comment = comment + + def __repr__(self): + return '' % self.comment + + +class IPC356_Parameter(object): + @classmethod + def from_line(cls, line): + if line[0] != 'P': + raise ValueError('Not a valid parameter statment') + splitline = line[2:].split() + parameter = splitline[0].strip() + value = ' '.join(splitline[1:]).strip() + return cls(parameter, value) + + def __init__(self, parameter, value): + self.parameter = parameter + self.value = value + + def __repr__(self): + return '' % (self.parameter, self.value) + + +class IPC356_TestRecord(object): + @classmethod + def from_line(cls, line, settings): + units = settings.units + angle = settings.angle_units + feature_types = {'1':'through-hole', '2': 'smt', + '3':'tooling-feature', '4':'tooling-hole'} + access = ['both', 'top', 'layer2', 'layer3', 'layer4', 'layer5', + 'layer6', 'layer7', 'bottom'] + record = {} + line = line.strip() + if line[0] != '3': + raise ValueError('Not a valid test record statment') + record['feature_type'] = feature_types[line[1]] + + end = len(line) - 1 if len(line) < 18 else 17 + record['net_name'] = line[3:end].strip() + + end = len(line) - 1 if len(line) < 27 else 26 + record['id'] = line[20:end].strip() + + end = len(line) - 1 if len(line) < 32 else 31 + record['pin'] = (line[27:end].strip() if line[27:end].strip() != '' + else None) + + record['location'] = 'middle' if line[31] == 'M' else 'end' + if line[32] == 'D': + end = len(line) - 1 if len(line) < 38 else 37 + dia = int(line[33:end].strip()) + record['hole_diameter'] = (dia * 0.0001 if units == 'inch' + else dia * 0.001) + if len(line) >= 38: + record['plated'] = (line[37] == 'P') + + if len(line) >= 40: + end = len(line) - 1 if len(line) < 42 else 41 + record['access'] = access[int(line[39:end])] + + if len(line) >= 43: + end = len(line) - 1 if len(line) < 50 else 49 + coord = int(line[42:49].strip()) + record['x_coord'] = (coord * 0.0001 if units == 'inch' + else coord * 0.001) + + if len(line) >= 51: + end = len(line) - 1 if len(line) < 58 else 57 + coord = int(line[50:57].strip()) + record['y_coord'] = (coord * 0.0001 if units == 'inch' + else coord * 0.001) + + if len(line) >= 59: + end = len(line) - 1 if len(line) < 63 else 62 + dim = line[58:62].strip() + if dim != '': + record['rect_x'] = (int(dim) * 0.0001 if units == 'inch' + else int(dim) * 0.001) + + if len(line) >= 64: + end = len(line) - 1 if len(line) < 68 else 67 + dim = line[63:67].strip() + if dim != '': + record['rect_y'] = (int(dim) * 0.0001 if units == 'inch' + else int(dim) * 0.001) + + if len(line) >= 69: + end = len(line) - 1 if len(line) < 72 else 71 + rot = line[68:71].strip() + if rot != '': + record['rect_rotation'] = (int(rot) if angle == 'degrees' + else math.degrees(rot)) + + if len(line) >= 74: + end = len(line) - 1 if len(line) < 75 else 74 + record['soldermask_info'] = line[73:74].strip() + + if len(line) >= 76: + end = len(line) - 1 if len(line < 80) else 79 + record['optional_info'] = line[75:end] + + return cls(**record) + + def __init__(self, **kwargs): + for key in kwargs: + setattr(self, key, kwargs[key]) + + def __repr__(self): + return '' % (self.net_name, + self.feature_type) + +class IPC356_BoardEdge(object): + + @classmethod + def from_line(cls, line, settings): + scale = 0.0001 if settings.units == 'inch' else 0.001 + points = [] + x = 0 + y = 0 + coord_strings = line.strip().split()[1:] + for coord in coord_strings: + coord_dict = _COORD.match(coord).groupdict() + x = int(coord_dict['x']) if coord_dict['x'] is not '' else x + y = int(coord_dict['y']) if coord_dict['y'] is not '' else y + points.append((x * scale, y * scale)) + return cls(points) + + def __init__(self, points): + self.points = points + + def __repr__(self): + return '' + + + +class IPC356_EndOfFile(object): + def __init__(self): + pass + + def to_netlist(self): + return '999' + + def __repr__(self): + return '' -- cgit From 68619d4d5a7beb38dc81d953b43bf4196ca1d3a6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 5 Mar 2015 22:42:42 -0500 Subject: Fix parsing for multiline ipc-d-356 records --- gerber/ipc356.py | 132 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 43 deletions(-) (limited to 'gerber/ipc356.py') diff --git a/gerber/ipc356.py b/gerber/ipc356.py index 2b6f1f6..1762480 100644 --- a/gerber/ipc356.py +++ b/gerber/ipc356.py @@ -18,7 +18,8 @@ import math import re -from .cam import FileSettings +from .cam import CamFile, FileSettings +from .primitives import TestRecord # Net Name Variables _NNAME = re.compile(r'^NNAME\d+$') @@ -44,7 +45,7 @@ def read(filename): return IPC_D_356.from_file(filename) -class IPC_D_356(object): +class IPC_D_356(CamFile): @classmethod def from_file(self, filename): @@ -52,10 +53,12 @@ class IPC_D_356(object): return p.parse(filename) - def __init__(self, statements, settings): + def __init__(self, statements, settings, primitives=None): self.statements = statements self.units = settings.units self.angle_units = settings.angle_units + self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name, + rec.access) for rec in self.test_records] @property def settings(self): @@ -98,6 +101,19 @@ class IPC_D_356(object): else: return None + + def render(self, ctx, layer='both', filename=None): + for p in self.primitives: + if layer == 'both' and p.layer in ('top', 'bottom', 'both'): + ctx.render(p) + elif layer == 'top' and p.layer in ('top', 'both'): + ctx.render(p) + elif layer == 'bottom' and p.layer in ('bottom', 'both'): + ctx.render(p) + if filename is not None: + ctx.dump(filename) + + class IPC_D_356_Parser(object): # TODO: Allow multi-line statements (e.g. Altium board edge) def __init__(self): @@ -112,51 +128,68 @@ class IPC_D_356_Parser(object): def parse(self, filename): with open(filename, 'r') as f: + oldline = '' for line in f: - - if line[0] == 'C': - # Comment - self.statements.append(IPC356_Comment.from_line(line)) - - elif line[0] == 'P': - # Parameter - p = IPC356_Parameter.from_line(line) - if p.parameter == 'UNITS': - if p.value in ('CUST', 'CUST 0'): - self.units = 'inch' - self.angle_units = 'degrees' - elif p.value == 'CUST 1': - self.units = 'metric' - self.angle_units = 'degrees' - elif p.value == 'CUST 2': - self.units = 'inch' - self.angle_units = 'radians' - self.statements.append(p) - if _NNAME.match(p.parameter): - # Add to list of net name variables - self.nnames[p.parameter] = p.value - - elif line[0] == '3' and line[2] == '7': - # Test Record - record = IPC356_TestRecord.from_line(line, self.settings) - - # Substitute net name variables - net = record.net_name - if (_NNAME.match(net) and net in self.nnames.keys()): - record.net_name = self.nnames[record.net_name] - self.statements.append(record) - - elif line[0:3] == '389': - # Altium Board Edge Info - self.statements.append(IPC356_BoardEdge.from_line(line, self.settings)) - - elif line[0] == '9': - self.multiline = False - self.statements.append(IPC356_EndOfFile()) + # Check for existing multiline data... + if oldline != '': + if len(line) and line[0] == '0': + oldline = oldline.rstrip('\r\n') + line[3:].rstrip() + else: + self._parse_line(oldline) + oldline = line + else: + oldline = line + self._parse_line(oldline) return IPC_D_356(self.statements, self.settings) + def _parse_line(self, line): + if not len(line): + return + if line[0] == 'C': + # Comment + self.statements.append(IPC356_Comment.from_line(line)) + + elif line[0] == 'P': + # Parameter + p = IPC356_Parameter.from_line(line) + if p.parameter == 'UNITS': + if p.value in ('CUST', 'CUST 0'): + self.units = 'inch' + self.angle_units = 'degrees' + elif p.value == 'CUST 1': + self.units = 'metric' + self.angle_units = 'degrees' + elif p.value == 'CUST 2': + self.units = 'inch' + self.angle_units = 'radians' + self.statements.append(p) + if _NNAME.match(p.parameter): + # Add to list of net name variables + self.nnames[p.parameter] = p.value + + elif line[0] == '9': + self.statements.append(IPC356_EndOfFile()) + + elif line[0:3] in ('317', '327', '367'): + # Test Record + record = IPC356_TestRecord.from_line(line, self.settings) + + # Substitute net name variables + net = record.net_name + if (_NNAME.match(net) and net in self.nnames.keys()): + record.net_name = self.nnames[record.net_name] + self.statements.append(record) + + elif line[0:3] == '379': + # Net Adjacency Info + pass + elif line[0:3] == '389': + # Altium Board Edge Info + self.statements.append(IPC356_BoardEdge.from_line(line, self.settings)) + + class IPC356_Comment(object): @classmethod def from_line(cls, line): @@ -302,6 +335,19 @@ class IPC356_BoardEdge(object): return '' +class IPC356_Adjacency(object): + + @classmethod + def from_line(cls, line): + nets = line.strip().split()[1:] + return cls(nets) + + def __init__(self, nets): + self.nets = nets + + def __repr__(self): + return '' + class IPC356_EndOfFile(object): def __init__(self): -- cgit From b5ce83beae4b7697c1b71faa2e616cf0e9598f60 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 6 Mar 2015 16:47:12 -0500 Subject: add rest of altium-supported ipc-d-356 statements --- gerber/ipc356.py | 134 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 25 deletions(-) (limited to 'gerber/ipc356.py') diff --git a/gerber/ipc356.py b/gerber/ipc356.py index 1762480..97d0bbd 100644 --- a/gerber/ipc356.py +++ b/gerber/ipc356.py @@ -27,6 +27,9 @@ _NNAME = re.compile(r'^NNAME\d+$') # Board Edge Coordinates _COORD = re.compile(r'X?(?P[\d\s]*)?Y?(?P[\d\s]*)?') +_SM_FIELD = {'0': 'none', '1': 'primary side', '2': 'secondary side', '3': 'both'} + + def read(filename): """ Read data from filename and return an IPC_D_356 @@ -81,8 +84,19 @@ class IPC_D_356(CamFile): @property def nets(self): - return list(set([rec.net_name for rec in self.test_records - if rec.net_name is not None])) + nets = [] + for net in list(set([rec.net_name for rec in self.test_records + if rec.net_name is not None])): + adjacent_nets = set() + for record in self.adjacency_records: + if record.net == net: + adjacent_nets = adjacent_nets.update(record.adjacent_nets) + elif net in record.adjacent_nets: + adjacent_nets.add(record.net) + nets.append(IPC356_Net(net, adjacent_nets)) + return nets + + @property def components(self): @@ -94,14 +108,17 @@ class IPC_D_356(CamFile): return [rec.id for rec in self.test_records if rec.id == 'VIA'] @property - def board_outline(self): - outline = [stmt for stmt in self.statements if isinstance(stmt, IPC356_BoardEdge)] - if len(outline): - return outline[0].points - else: - return None + def outlines(self): + return [stmt for stmt in self.statements + if isinstance(stmt, IPC356_Outline)] + + @property + def adjacency_records(self): + return [record for record in self.statements + if isinstance(record, IPC356_Adjacency)] + def render(self, ctx, layer='both', filename=None): for p in self.primitives: if layer == 'both' and p.layer in ('top', 'bottom', 'both'): @@ -182,12 +199,17 @@ class IPC_D_356_Parser(object): record.net_name = self.nnames[record.net_name] self.statements.append(record) + elif line[0:3] == '378': + # Conductor + self.statements.append(IPC356_Conductor.from_line(line, self.settings)) + elif line[0:3] == '379': - # Net Adjacency Info - pass + # Net Adjacency + self.statements.append(IPC356_Adjacency.from_line(line)) + elif line[0:3] == '389': - # Altium Board Edge Info - self.statements.append(IPC356_BoardEdge.from_line(line, self.settings)) + # Outline + self.statements.append(IPC356_Outline.from_line(line, self.settings)) class IPC356_Comment(object): @@ -296,7 +318,8 @@ class IPC356_TestRecord(object): if len(line) >= 74: end = len(line) - 1 if len(line) < 75 else 74 - record['soldermask_info'] = line[73:74].strip() + sm_info = line[73:74].strip() + record['soldermask_info'] = _SM_FIELD.get(sm_info) if len(line) >= 76: end = len(line) - 1 if len(line < 80) else 79 @@ -309,13 +332,14 @@ class IPC356_TestRecord(object): setattr(self, key, kwargs[key]) def __repr__(self): - return '' % (self.net_name, + return '' % (self.net_name, self.feature_type) -class IPC356_BoardEdge(object): +class IPC356_Outline(object): @classmethod def from_line(cls, line, settings): + type = line[3:17].strip() scale = 0.0001 if settings.units == 'inch' else 0.001 points = [] x = 0 @@ -326,27 +350,78 @@ class IPC356_BoardEdge(object): x = int(coord_dict['x']) if coord_dict['x'] is not '' else x y = int(coord_dict['y']) if coord_dict['y'] is not '' else y points.append((x * scale, y * scale)) - return cls(points) + return cls(type, points) - def __init__(self, points): + def __init__(self, type, points): + self.type = type self.points = points def __repr__(self): - return '' + return '' % self.type + + +class IPC356_Conductor(object): + @classmethod + def from_line(cls, line, settings): + if line[0:3] != '378': + raise ValueError('Not a valid IPC-D-356 Conductor statement') + + scale = 0.0001 if settings.units == 'inch' else 0.001 + net_name = line[3:17].strip() + layer = int(line[19:21]) + + # Parse out aperture definiting + raw_aperture = line[22:].split()[0] + aperture_dict = _COORD.match(raw_aperture).groupdict() + x = 0 + y = 0 + x = int(aperture_dict['x']) * scale if aperture_dict['x'] is not '' else None + y = int(aperture_dict['y']) * scale if aperture_dict['y'] is not '' else None + aperture = (x, y) + + # Parse out conductor shapes + shapes = [] + coord_list = ' '.join(line[22:].split()[1:]) + raw_shapes = coord_list.split('*') + for rshape in raw_shapes: + x = 0 + y = 0 + shape = [] + coords = rshape.split() + for coord in coords: + coord_dict = _COORD.match(coord).groupdict() + x = int(coord_dict['x']) if coord_dict['x'] is not '' else x + y = int(coord_dict['y']) if coord_dict['y'] is not '' else y + shape.append((x * scale, y * scale)) + shapes.append(tuple(shape)) + return cls(net_name, layer, aperture, tuple(shapes)) + + def __init__(self, net_name, layer, aperture, shapes): + self.net_name = net_name + self.layer = layer + self.aperture = aperture + self.shapes = shapes + + def __repr__(self): + return '' % self.net_name class IPC356_Adjacency(object): @classmethod def from_line(cls, line): - nets = line.strip().split()[1:] - return cls(nets) - - def __init__(self, nets): - self.nets = nets - + if line[0:3] != '379': + raise ValueError('Not a valid IPC-D-356 Conductor statement') + nets = line[3:].strip().split() + + return cls(nets[0], nets[1:]) + + def __init__(self, net, adjacent_nets): + self.net = net + self.adjacent_nets = adjacent_nets + def __repr__(self): - return '' + return '' % self.net class IPC356_EndOfFile(object): @@ -358,3 +433,12 @@ class IPC356_EndOfFile(object): def __repr__(self): return '' + +class IPC356_Net(object): + def __init__(self, name, adjacent_nets): + self.name = name + self.adjacent_nets = set(adjacent_nets) if adjacent_nets is not None else set() + + + def __repr__(self): + return '' % self.name -- cgit From faa44ab73135ee111b9856dcdd155540cb67cfc3 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 1 Jun 2015 20:58:16 -0400 Subject: Fix IPC-D-356 parser. Handle too-long reference designators exported by eagle per #28. --- gerber/ipc356.py | 70 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 33 deletions(-) (limited to 'gerber/ipc356.py') diff --git a/gerber/ipc356.py b/gerber/ipc356.py index 97d0bbd..e4d8027 100644 --- a/gerber/ipc356.py +++ b/gerber/ipc356.py @@ -248,6 +248,7 @@ class IPC356_Parameter(object): class IPC356_TestRecord(object): @classmethod def from_line(cls, line, settings): + offset = 0 units = settings.units angle = settings.angle_units feature_types = {'1':'through-hole', '2': 'smt', @@ -263,67 +264,70 @@ class IPC356_TestRecord(object): end = len(line) - 1 if len(line) < 18 else 17 record['net_name'] = line[3:end].strip() - end = len(line) - 1 if len(line) < 27 else 26 + if len(line) >= 27 and line[26] != '-': + offset = line[26:].find('-') + offset = 0 if offset == -1 else offset + end = len(line) - 1 if len(line) < (27 + offset) else (26 + offset) record['id'] = line[20:end].strip() - end = len(line) - 1 if len(line) < 32 else 31 - record['pin'] = (line[27:end].strip() if line[27:end].strip() != '' + end = len(line) - 1 if len(line) < (32 + offset) else (31 + offset) + record['pin'] = (line[27 + offset:end].strip() if line[27 + offset:end].strip() != '' else None) - record['location'] = 'middle' if line[31] == 'M' else 'end' - if line[32] == 'D': - end = len(line) - 1 if len(line) < 38 else 37 - dia = int(line[33:end].strip()) + record['location'] = 'middle' if line[31 + offset] == 'M' else 'end' + if line[32 + offset] == 'D': + end = len(line) - 1 if len(line) < (38 + offset) else (37 + offset) + dia = int(line[33 + offset:end].strip()) record['hole_diameter'] = (dia * 0.0001 if units == 'inch' else dia * 0.001) - if len(line) >= 38: - record['plated'] = (line[37] == 'P') + if len(line) >= (38 + offset): + record['plated'] = (line[37 + offset] == 'P') - if len(line) >= 40: - end = len(line) - 1 if len(line) < 42 else 41 - record['access'] = access[int(line[39:end])] + if len(line) >= (40 + offset): + end = len(line) - 1 if len(line) < (42 + offset) else (41 + offset) + record['access'] = access[int(line[39 + offset:end])] - if len(line) >= 43: - end = len(line) - 1 if len(line) < 50 else 49 - coord = int(line[42:49].strip()) + if len(line) >= (43 + offset): + end = len(line) - 1 if len(line) < (50 + offset) else (49 + offset) + coord = int(line[42 + offset:end].strip()) record['x_coord'] = (coord * 0.0001 if units == 'inch' else coord * 0.001) - if len(line) >= 51: - end = len(line) - 1 if len(line) < 58 else 57 - coord = int(line[50:57].strip()) + if len(line) >= (51 + offset): + end = len(line) - 1 if len(line) < (58 + offset) else (57 + offset) + coord = int(line[50 + offset:end].strip()) record['y_coord'] = (coord * 0.0001 if units == 'inch' else coord * 0.001) - if len(line) >= 59: - end = len(line) - 1 if len(line) < 63 else 62 - dim = line[58:62].strip() + if len(line) >= (59 + offset): + end = len(line) - 1 if len(line) < (63 + offset) else (62 + offset) + dim = line[58 + offset:end].strip() if dim != '': record['rect_x'] = (int(dim) * 0.0001 if units == 'inch' else int(dim) * 0.001) - if len(line) >= 64: - end = len(line) - 1 if len(line) < 68 else 67 - dim = line[63:67].strip() + if len(line) >= (64 + offset): + end = len(line) - 1 if len(line) < (68 + offset) else (67 + offset) + dim = line[63 + offset:end].strip() if dim != '': record['rect_y'] = (int(dim) * 0.0001 if units == 'inch' else int(dim) * 0.001) - if len(line) >= 69: - end = len(line) - 1 if len(line) < 72 else 71 - rot = line[68:71].strip() + if len(line) >= (69 + offset): + end = len(line) - 1 if len(line) < (72 + offset) else (71 + offset) + rot = line[68 + offset:end].strip() if rot != '': record['rect_rotation'] = (int(rot) if angle == 'degrees' else math.degrees(rot)) - if len(line) >= 74: - end = len(line) - 1 if len(line) < 75 else 74 - sm_info = line[73:74].strip() + if len(line) >= (74 + offset): + end = 74 + offset + sm_info = line[73 + offset:end].strip() record['soldermask_info'] = _SM_FIELD.get(sm_info) - if len(line) >= 76: - end = len(line) - 1 if len(line < 80) else 79 - record['optional_info'] = line[75:end] + if len(line) >= (76 + offset): + end = len(line) - 1 if len(line) < (80 + offset) else 79 + offset + record['optional_info'] = line[75 + offset:end] return cls(**record) -- cgit From 6e29b9bcae8167dbb9c75e5a79e09886b952e988 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Sun, 15 Nov 2015 22:28:56 -0200 Subject: Use Python's universal newlines to open files --- gerber/ipc356.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'gerber/ipc356.py') diff --git a/gerber/ipc356.py b/gerber/ipc356.py index e4d8027..b8a7ba3 100644 --- a/gerber/ipc356.py +++ b/gerber/ipc356.py @@ -144,7 +144,7 @@ class IPC_D_356_Parser(object): return FileSettings(units=self.units, angle_units=self.angle_units) def parse(self, filename): - with open(filename, 'r') as f: + with open(filename, 'rU') as f: oldline = '' for line in f: # Check for existing multiline data... -- cgit From 6f876edd09d9b81649691e529f85653f14b8fd1c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 22 Dec 2015 02:45:48 -0500 Subject: Add PCB interface this incorporates some of @chintal's layers.py changes PCB.from_directory() simplifies loading of multiple gerbers the PCB() class should be pretty helpful going forward... the context classes could use some cleaning up, although I'd like to wait until the freecad stuff gets merged, that way we can try to refactor the context base to support more use cases --- gerber/ipc356.py | 89 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 38 deletions(-) (limited to 'gerber/ipc356.py') diff --git a/gerber/ipc356.py b/gerber/ipc356.py index b8a7ba3..7dadd22 100644 --- a/gerber/ipc356.py +++ b/gerber/ipc356.py @@ -27,8 +27,11 @@ _NNAME = re.compile(r'^NNAME\d+$') # Board Edge Coordinates _COORD = re.compile(r'X?(?P[\d\s]*)?Y?(?P[\d\s]*)?') -_SM_FIELD = {'0': 'none', '1': 'primary side', '2': 'secondary side', '3': 'both'} - +_SM_FIELD = { + '0': 'none', + '1': 'primary side', + '2': 'secondary side', + '3': 'both'} def read(filename): @@ -51,17 +54,17 @@ def read(filename): class IPC_D_356(CamFile): @classmethod - def from_file(self, filename): - p = IPC_D_356_Parser() - return p.parse(filename) - + def from_file(cls, filename): + parser = IPC_D_356_Parser() + return parser.parse(filename) - def __init__(self, statements, settings, primitives=None): + def __init__(self, statements, settings, primitives=None, filename=None): self.statements = statements self.units = settings.units self.angle_units = settings.angle_units self.primitives = [TestRecord((rec.x_coord, rec.y_coord), rec.net_name, rec.access) for rec in self.test_records] + self.filename = filename @property def settings(self): @@ -95,8 +98,6 @@ class IPC_D_356(CamFile): adjacent_nets.add(record.net) nets.append(IPC356_Net(net, adjacent_nets)) return nets - - @property def components(self): @@ -109,14 +110,12 @@ class IPC_D_356(CamFile): @property def outlines(self): - return [stmt for stmt in self.statements + return [stmt for stmt in self.statements if isinstance(stmt, IPC356_Outline)] - - @property def adjacency_records(self): - return [record for record in self.statements + return [record for record in self.statements if isinstance(record, IPC356_Adjacency)] def render(self, ctx, layer='both', filename=None): @@ -133,6 +132,7 @@ class IPC_D_356(CamFile): class IPC_D_356_Parser(object): # TODO: Allow multi-line statements (e.g. Altium board edge) + def __init__(self): self.units = 'inch' self.angle_units = 'degrees' @@ -158,8 +158,7 @@ class IPC_D_356_Parser(object): oldline = line self._parse_line(oldline) - return IPC_D_356(self.statements, self.settings) - + return IPC_D_356(self.statements, self.settings, filename=filename) def _parse_line(self, line): if not len(line): @@ -201,18 +200,23 @@ class IPC_D_356_Parser(object): elif line[0:3] == '378': # Conductor - self.statements.append(IPC356_Conductor.from_line(line, self.settings)) - + self.statements.append( + IPC356_Conductor.from_line( + line, self.settings)) + elif line[0:3] == '379': # Net Adjacency self.statements.append(IPC356_Adjacency.from_line(line)) - + elif line[0:3] == '389': # Outline - self.statements.append(IPC356_Outline.from_line(line, self.settings)) + self.statements.append( + IPC356_Outline.from_line( + line, self.settings)) class IPC356_Comment(object): + @classmethod def from_line(cls, line): if line[0] != 'C': @@ -228,6 +232,7 @@ class IPC356_Comment(object): class IPC356_Parameter(object): + @classmethod def from_line(cls, line): if line[0] != 'P': @@ -246,13 +251,14 @@ class IPC356_Parameter(object): class IPC356_TestRecord(object): + @classmethod def from_line(cls, line, settings): offset = 0 units = settings.units angle = settings.angle_units - feature_types = {'1':'through-hole', '2': 'smt', - '3':'tooling-feature', '4':'tooling-hole'} + feature_types = {'1': 'through-hole', '2': 'smt', + '3': 'tooling-feature', '4': 'tooling-hole'} access = ['both', 'top', 'layer2', 'layer3', 'layer4', 'layer5', 'layer6', 'layer7', 'bottom'] record = {} @@ -290,21 +296,21 @@ class IPC356_TestRecord(object): if len(line) >= (43 + offset): end = len(line) - 1 if len(line) < (50 + offset) else (49 + offset) coord = int(line[42 + offset:end].strip()) - record['x_coord'] = (coord * 0.0001 if units == 'inch' + record['x_coord'] = (coord * 0.0001 if units == 'inch' else coord * 0.001) if len(line) >= (51 + offset): end = len(line) - 1 if len(line) < (58 + offset) else (57 + offset) coord = int(line[50 + offset:end].strip()) - record['y_coord'] = (coord * 0.0001 if units == 'inch' - else coord * 0.001) + record['y_coord'] = (coord * 0.0001 if units == 'inch' + else coord * 0.001) if len(line) >= (59 + offset): end = len(line) - 1 if len(line) < (63 + offset) else (62 + offset) dim = line[58 + offset:end].strip() if dim != '': record['rect_x'] = (int(dim) * 0.0001 if units == 'inch' - else int(dim) * 0.001) + else int(dim) * 0.001) if len(line) >= (64 + offset): end = len(line) - 1 if len(line) < (68 + offset) else (67 + offset) @@ -321,7 +327,7 @@ class IPC356_TestRecord(object): else math.degrees(rot)) if len(line) >= (74 + offset): - end = 74 + offset + end = 74 + offset sm_info = line[73 + offset:end].strip() record['soldermask_info'] = _SM_FIELD.get(sm_info) @@ -337,7 +343,8 @@ class IPC356_TestRecord(object): def __repr__(self): return '' % (self.net_name, - self.feature_type) + self.feature_type) + class IPC356_Outline(object): @@ -365,24 +372,27 @@ class IPC356_Outline(object): class IPC356_Conductor(object): + @classmethod def from_line(cls, line, settings): if line[0:3] != '378': raise ValueError('Not a valid IPC-D-356 Conductor statement') - + scale = 0.0001 if settings.units == 'inch' else 0.001 net_name = line[3:17].strip() layer = int(line[19:21]) - + # Parse out aperture definiting raw_aperture = line[22:].split()[0] aperture_dict = _COORD.match(raw_aperture).groupdict() x = 0 y = 0 - x = int(aperture_dict['x']) * scale if aperture_dict['x'] is not '' else None - y = int(aperture_dict['y']) * scale if aperture_dict['y'] is not '' else None + x = int(aperture_dict['x']) * \ + scale if aperture_dict['x'] is not '' else None + y = int(aperture_dict['y']) * \ + scale if aperture_dict['y'] is not '' else None aperture = (x, y) - + # Parse out conductor shapes shapes = [] coord_list = ' '.join(line[22:].split()[1:]) @@ -399,7 +409,7 @@ class IPC356_Conductor(object): shape.append((x * scale, y * scale)) shapes.append(tuple(shape)) return cls(net_name, layer, aperture, tuple(shapes)) - + def __init__(self, net_name, layer, aperture, shapes): self.net_name = net_name self.layer = layer @@ -417,18 +427,19 @@ class IPC356_Adjacency(object): if line[0:3] != '379': raise ValueError('Not a valid IPC-D-356 Conductor statement') nets = line[3:].strip().split() - + return cls(nets[0], nets[1:]) def __init__(self, net, adjacent_nets): self.net = net self.adjacent_nets = adjacent_nets - + def __repr__(self): return '' % self.net class IPC356_EndOfFile(object): + def __init__(self): pass @@ -437,12 +448,14 @@ class IPC356_EndOfFile(object): def __repr__(self): return '' - + + class IPC356_Net(object): + def __init__(self, name, adjacent_nets): self.name = name - self.adjacent_nets = set(adjacent_nets) if adjacent_nets is not None else set() - + self.adjacent_nets = set( + adjacent_nets) if adjacent_nets is not None else set() def __repr__(self): return '' % self.name -- cgit