From 22a6f87e94c1192b277a1353aefc7c0316f41f90 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 6 Oct 2014 18:28:32 -0400 Subject: add excellon file write --- gerber/excellon.py | 23 +-- gerber/excellon_statements.py | 54 +++--- gerber/utils.py | 385 +++++++++++++++++++++--------------------- 3 files changed, 239 insertions(+), 223 deletions(-) diff --git a/gerber/excellon.py b/gerber/excellon.py index 45a8e4b..072bc31 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -74,6 +74,11 @@ class ExcellonFile(CncFile): ctx.drill(pos[0], pos[1], tool.diameter) ctx.dump(filename) + def write(self, filename): + with open(filename, 'w') as f: + for statement in self.statements: + f.write(statement.to_excellon() + '\n') + class ExcellonParser(object): """ Excellon File Parser @@ -155,9 +160,9 @@ class ExcellonParser(object): elif line[0] == 'T' and self.state != 'HEADER': stmt = ToolSelectionStmt.from_excellon(line) - self.active_tool self.tools[stmt.tool] + self.active_tool = self.tools[stmt.tool] #self.active_tool = self.tools[int(line.strip().split('T')[1])] - self.statements.append(statement) + self.statements.append(stmt) elif line[0] in ['X', 'Y']: stmt = CoordinateStmt.from_excellon(line, fmt, zs) @@ -197,18 +202,8 @@ class ExcellonParser(object): return FileSettings(units=self.units, format=self.format, zero_suppression=self.zero_suppression, notation=self.notation) - - -def pairwise(iterator): - """ Iterate over list taking two elements at a time. - - e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)] - """ - itr = iter(iterator) - while True: - yield tuple([itr.next() for i in range(2)]) if __name__ == '__main__': - p = parser() - p.parse('examples/ncdrill.txt') + p = ExcellonParser() + parsed = p.parse('examples/ncdrill.txt') diff --git a/gerber/excellon_statements.py b/gerber/excellon_statements.py index faadd20..09b72c1 100644 --- a/gerber/excellon_statements.py +++ b/gerber/excellon_statements.py @@ -15,10 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .utils import write_gerber_value +from .utils import parse_gerber_value, write_gerber_value, decimal_string +import re -__all__ = ['ExcellonTool', 'ToolSelectionStatment', 'CoordinateStmt', +__all__ = ['ExcellonTool', 'ToolSelectionStmt', 'CoordinateStmt', 'CommentStmt', 'HeaderBeginStmt', 'HeaderEndStmt', 'RewindStopStmt', 'EndOfProgramStmt', 'UnitStmt', 'IncrementalModeStmt', 'VersionStmt', 'FormatStmt', 'LinkToolStmt', @@ -138,20 +139,20 @@ class ExcellonTool(ExcellonStatement): fmt = self.settings['format'] zs = self.settings['zero_suppression'] stmt = 'T%d' % self.number - if self.retract_rate: + if self.retract_rate is not None: stmt += 'B%s' % write_gerber_value(self.retract_rate, fmt, zs) - if self.diameter: - stmt += 'C%s' % write_gerber_value(self.diameter, fmt, zs) - if self.feed_rate: + if self.feed_rate is not None: stmt += 'F%s' % write_gerber_value(self.feed_rate, fmt, zs) - if self.max_hit_count: + if self.max_hit_count is not None: stmt += 'H%s' % write_gerber_value(self.max_hit_count, fmt, zs) - if self.rpm: + if self.rpm is not None: if self.rpm < 100000.: stmt += 'S%s' % write_gerber_value(self.rpm / 1000., fmt, zs) else: stmt += 'S%g' % self.rpm / 1000. - if self.depth_offset: + if self.diameter is not None: + stmt += 'C%s' % decimal_string(self.diameter, 5, True) + if self.depth_offset is not None: stmt += 'Z%s' % write_gerber_value(self.depth_offset, fmt, zs) return stmt @@ -163,7 +164,7 @@ class ExcellonTool(ExcellonStatement): return '' % (self.number, self.diameter, unit) -class ToolSelectionStatment(ExcellonStatement): +class ToolSelectionStmt(ExcellonStatement): @classmethod def from_excellon(cls, line): @@ -189,6 +190,7 @@ class ToolSelectionStatment(ExcellonStatement): class CoordinateStmt(ExcellonStatement): + @classmethod def from_excellon(cls, line, format=(2, 5), zero_suppression='trailing'): x = None y = None @@ -208,22 +210,23 @@ class CoordinateStmt(ExcellonStatement): def to_excellon(self): stmt = '' if self.x is not None: - stmt.append('X%s' % write_gerber_value(self.x)) + stmt += 'X%s' % write_gerber_value(self.x) if self.y is not None: - stmt.append('Y%s' % write_gerber_value(self.y)) + stmt += 'Y%s' % write_gerber_value(self.y) return stmt class CommentStmt(ExcellonStatement): - def from_excellon(self, line): + @classmethod + def from_excellon(cls, line): return cls(line.strip().lstrip(';')) def __init__(self, comment): self.comment = comment def to_excellon(self): - return ';%s' % comment + return ';%s' % self.comment class HeaderBeginStmt(ExcellonStatement): @@ -265,7 +268,7 @@ class EndOfProgramStmt(ExcellonStatement): stmt += 'X%s' % write_gerber_value(self.x) if self.y is not None: stmt += 'Y%s' % write_gerber_value(self.y) - + return stmt class UnitStmt(ExcellonStatement): @@ -281,8 +284,9 @@ class UnitStmt(ExcellonStatement): def to_excellon(self): stmt = '%s,%s' % ('INCH' if self.units == 'inch' else 'METRIC', - 'LZ' if self.zero_suppression == 'trailing' else 'TZ') - + 'LZ' if self.zero_suppression == 'trailing' + else 'TZ') + return stmt class IncrementalModeStmt(ExcellonStatement): @@ -292,7 +296,7 @@ class IncrementalModeStmt(ExcellonStatement): def __init__(self, mode='off'): if mode.lower() not in ['on', 'off']: - raise ValueError('Mode may be "on" or "off") + raise ValueError('Mode may be "on" or "off"') self.mode = 'off' def to_excellon(self): @@ -309,7 +313,7 @@ class VersionStmt(ExcellonStatement): def __init__(self, version=1): version = int(version) if version not in [1, 2]: - raise ValueError('Valid versions are 1 or 2' + raise ValueError('Valid versions are 1 or 2') self.version = version def to_excellon(self): @@ -374,3 +378,15 @@ class UnknownStmt(ExcellonStatement): def to_excellon(self): return self.stmt + + + + +def pairwise(iterator): + """ Iterate over list taking two elements at a time. + + e.g. [1, 2, 3, 4, 5, 6] ==> [(1, 2), (3, 4), (5, 6)] + """ + itr = iter(iterator) + while True: + yield tuple([itr.next() for i in range(2)]) \ No newline at end of file diff --git a/gerber/utils.py b/gerber/utils.py index 625a9e1..1721a7d 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -1,190 +1,195 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -gerber.utils -============ -**Gerber and Excellon file handling utilities** - -This module provides utility functions for working with Gerber and Excellon -files. -""" - -# Author: Hamilton Kibbe -# License: - - -def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): - """ Convert gerber/excellon formatted string to floating-point number - - .. note:: - Format and zero suppression are configurable. Note that the Excellon - and Gerber formats use opposite terminology with respect to leading - and trailing zeros. The Gerber format specifies which zeros are - suppressed, while the Excellon format specifies which zeros are - included. This function uses the Gerber-file convention, so an - Excellon file in LZ (leading zeros) mode would use - `zero_suppression='trailing'` - - - Parameters - ---------- - value : string - A Gerber/Excellon-formatted string representing a numerical value. - - format : tuple (int,int) - Gerber/Excellon precision format expressed as a tuple containing: - (number of integer-part digits, number of decimal-part digits) - - zero_suppression : string - Zero-suppression mode. May be 'leading' or 'trailing' - - Returns - ------- - value : float - The specified value as a floating-point number. - - """ - # Format precision - integer_digits, decimal_digits = format - MAX_DIGITS = integer_digits + decimal_digits - - # Absolute maximum number of digits supported. This will handle up to - # 6:7 format, which is somewhat supported, even though the gerber spec - # only allows up to 6:6 - if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: - raise ValueError('Parser only supports precision up to 6:7 format') - - # Remove extraneous information - value = value.strip() - value = value.strip(' +') - negative = '-' in value - if negative: - value = value.strip(' -') - - # Handle excellon edge case with explicit decimal. "That was easy!" - if '.' in value: - return float(value) - - digits = [digit for digit in '0' * MAX_DIGITS] - offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) - for i, digit in enumerate(value): - digits[i + offset] = digit - - result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) - return -1.0 * result if negative else result - - -def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): - """ Convert a floating point number to a Gerber/Excellon-formatted string. - - .. note:: - Format and zero suppression are configurable. Note that the Excellon - and Gerber formats use opposite terminology with respect to leading - and trailing zeros. The Gerber format specifies which zeros are - suppressed, while the Excellon format specifies which zeros are - included. This function uses the Gerber-file convention, so an - Excellon file in LZ (leading zeros) mode would use - `zero_suppression='trailing'` - - Parameters - ---------- - value : float - A floating point value. - - format : tuple (n=2) - Gerber/Excellon precision format expressed as a tuple containing: - (number of integer-part digits, number of decimal-part digits) - - zero_suppression : string - Zero-suppression mode. May be 'leading' or 'trailing' - - Returns - ------- - value : string - The specified value as a Gerber/Excellon-formatted string. - """ - # Format precision - integer_digits, decimal_digits = format - MAX_DIGITS = integer_digits + decimal_digits - - if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: - raise ValueError('Parser only supports precision up to 6:7 format') - - # negative sign affects padding, so deal with it at the end... - negative = value < 0.0 - if negative: - value = -1.0 * value - - # Format string for padding out in both directions - fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits) - - digits = [val for val in fmtstring % value if val != '.'] - - # Suppression... - if zero_suppression == 'trailing': - while digits[-1] == '0': - digits.pop() - else: - while digits[0] == '0': - digits.pop(0) - - return ''.join(digits) if not negative else ''.join(['-'] + digits) - - -def decimal_string(value, precision=6): - """ Convert float to string with limited precision - - Parameters - ---------- - value : float - A floating point value. - - precision : - Maximum number of decimal places to print - - Returns - ------- - value : string - The specified value as a string. - - """ - floatstr = '%0.20g' % value - integer = None - decimal = None - if '.' in floatstr: - integer, decimal = floatstr.split('.') - elif ',' in floatstr: - integer, decimal = floatstr.split(',') - if len(decimal) > precision: - decimal = decimal[:precision] - if integer or decimal: - return ''.join([integer, '.', decimal]) - else: - return int(floatstr) - - -def detect_file_format(filename): - """ Determine format of a file - - Parameters - ---------- - filename : string - Filename of the file to read. - - Returns - ------- - format : string - File format. either 'excellon' or 'rs274x' - """ - - # Read the first 20 lines - with open(filename, 'r') as f: - lines = [next(f) for x in xrange(20)] - - # Look for - for line in lines: - if 'M48' in line: - return 'excellon' - elif '%FS' in line: - return'rs274x' - return 'unknown' +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +gerber.utils +============ +**Gerber and Excellon file handling utilities** + +This module provides utility functions for working with Gerber and Excellon +files. +""" + +# Author: Hamilton Kibbe +# License: + + +def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): + """ Convert gerber/excellon formatted string to floating-point number + + .. note:: + Format and zero suppression are configurable. Note that the Excellon + and Gerber formats use opposite terminology with respect to leading + and trailing zeros. The Gerber format specifies which zeros are + suppressed, while the Excellon format specifies which zeros are + included. This function uses the Gerber-file convention, so an + Excellon file in LZ (leading zeros) mode would use + `zero_suppression='trailing'` + + + Parameters + ---------- + value : string + A Gerber/Excellon-formatted string representing a numerical value. + + format : tuple (int,int) + Gerber/Excellon precision format expressed as a tuple containing: + (number of integer-part digits, number of decimal-part digits) + + zero_suppression : string + Zero-suppression mode. May be 'leading' or 'trailing' + + Returns + ------- + value : float + The specified value as a floating-point number. + + """ + # Format precision + integer_digits, decimal_digits = format + MAX_DIGITS = integer_digits + decimal_digits + + # Absolute maximum number of digits supported. This will handle up to + # 6:7 format, which is somewhat supported, even though the gerber spec + # only allows up to 6:6 + if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: + raise ValueError('Parser only supports precision up to 6:7 format') + + # Remove extraneous information + value = value.strip() + value = value.strip(' +') + negative = '-' in value + if negative: + value = value.strip(' -') + + # Handle excellon edge case with explicit decimal. "That was easy!" + if '.' in value: + return float(value) + + digits = [digit for digit in '0' * MAX_DIGITS] + offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) + for i, digit in enumerate(value): + digits[i + offset] = digit + + result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) + return -1.0 * result if negative else result + + +def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): + """ Convert a floating point number to a Gerber/Excellon-formatted string. + + .. note:: + Format and zero suppression are configurable. Note that the Excellon + and Gerber formats use opposite terminology with respect to leading + and trailing zeros. The Gerber format specifies which zeros are + suppressed, while the Excellon format specifies which zeros are + included. This function uses the Gerber-file convention, so an + Excellon file in LZ (leading zeros) mode would use + `zero_suppression='trailing'` + + Parameters + ---------- + value : float + A floating point value. + + format : tuple (n=2) + Gerber/Excellon precision format expressed as a tuple containing: + (number of integer-part digits, number of decimal-part digits) + + zero_suppression : string + Zero-suppression mode. May be 'leading' or 'trailing' + + Returns + ------- + value : string + The specified value as a Gerber/Excellon-formatted string. + """ + # Format precision + integer_digits, decimal_digits = format + MAX_DIGITS = integer_digits + decimal_digits + + if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: + raise ValueError('Parser only supports precision up to 6:7 format') + + # Edge case... + if value == 0: + return '00' + + # negative sign affects padding, so deal with it at the end... + negative = value < 0.0 + if negative: + value = -1.0 * value + + # Format string for padding out in both directions + fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits) + digits = [val for val in fmtstring % value if val != '.'] + + # Suppression... + if zero_suppression == 'trailing': + while digits[-1] == '0': + digits.pop() + else: + while digits[0] == '0': + digits.pop(0) + + return ''.join(digits) if not negative else ''.join(['-'] + digits) + + +def decimal_string(value, precision=6, padding=False): + """ Convert float to string with limited precision + + Parameters + ---------- + value : float + A floating point value. + + precision : + Maximum number of decimal places to print + + Returns + ------- + value : string + The specified value as a string. + + """ + floatstr = '%0.10g' % value + integer = None + decimal = None + if '.' in floatstr: + integer, decimal = floatstr.split('.') + elif ',' in floatstr: + integer, decimal = floatstr.split(',') + if len(decimal) > precision: + decimal = decimal[:precision] + elif padding: + decimal = decimal + (precision - len(decimal)) * '0' + if integer or decimal: + return ''.join([integer, '.', decimal]) + else: + return int(floatstr) + + +def detect_file_format(filename): + """ Determine format of a file + + Parameters + ---------- + filename : string + Filename of the file to read. + + Returns + ------- + format : string + File format. either 'excellon' or 'rs274x' + """ + + # Read the first 20 lines + with open(filename, 'r') as f: + lines = [next(f) for x in xrange(20)] + + # Look for + for line in lines: + if 'M48' in line: + return 'excellon' + elif '%FS' in line: + return'rs274x' + return 'unknown' -- cgit