Allow files to be read from strings per #37

Adds a loads() method to the top level module which generates a GerberFile or ExcellonFile from a string
refactor
Hamilton Kibbe 2015-10-10 16:51:21 -04:00
rodzic b81c9d4bf9
commit dd63b169f1
10 zmienionych plików z 140 dodań i 53 usunięć

Wyświetl plik

@ -30,9 +30,6 @@ from gerber.render import GerberCairoContext
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
# Open the gerber files
copper = read(os.path.join(GERBER_FOLDER, 'copper.GTL'))
mask = read(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))

Wyświetl plik

@ -23,4 +23,4 @@ gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon
files in python.
"""
from .common import read
from .common import read, loads

Wyświetl plik

@ -15,6 +15,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from . import rs274x
from . import excellon
from .utils import detect_file_format
def read(filename):
""" Read a gerber or excellon file and return a representative object.
@ -30,10 +34,9 @@ def read(filename):
CncFile object representing the file, either GerberFile or
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
"""
from . import rs274x
from . import excellon
from .utils import detect_file_format
fmt = detect_file_format(filename)
with open(filename, 'r') as f:
data = f.read()
fmt = detect_file_format(data)
if fmt == 'rs274x':
return rs274x.read(filename)
elif fmt == 'excellon':
@ -41,3 +44,28 @@ def read(filename):
else:
raise TypeError('Unable to detect file format')
def loads(data):
""" Read gerber or excellon file contents from a string and return a
representative object.
Parameters
----------
data : string
gerber or excellon file contents as a string.
Returns
-------
file : CncFile subclass
CncFile object representing the file, either GerberFile or
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
"""
fmt = detect_file_format(data)
if fmt == 'rs274x':
return rs274x.loads(data)
elif fmt == 'excellon':
return excellon.loads(data)
else:
raise TypeError('Unable to detect file format')

Wyświetl plik

@ -25,6 +25,7 @@ This module provides Excellon file classes and parsing utilities
import math
import operator
from cStringIO import StringIO
from .excellon_statements import *
from .cam import CamFile, FileSettings
@ -46,9 +47,28 @@ def read(filename):
"""
# File object should use settings from source file by default.
settings = FileSettings(**detect_excellon_format(filename))
with open(filename, 'r') as f:
data = f.read()
settings = FileSettings(**detect_excellon_format(data))
return ExcellonParser(settings).parse(filename)
def loads(data):
""" Read data from string and return an ExcellonFile
Parameters
----------
data : string
string containing Excellon file contents
Returns
-------
file : :class:`gerber.excellon.ExcellonFile`
An ExcellonFile created from the specified file.
"""
# File object should use settings from source file by default.
settings = FileSettings(**detect_excellon_format(data))
return ExcellonParser(settings).parse_raw(data)
class DrillHit(object):
def __init__(self, tool, position):
@ -302,9 +322,12 @@ class ExcellonParser(object):
def parse(self, filename):
with open(filename, 'r') as f:
for line in f:
self._parse(line.strip())
data = f.read()
return self.parse_raw(data, filename)
def parse_raw(self, data, filename=None):
for line in StringIO(data):
self._parse(line.strip())
for stmt in self.statements:
stmt.units = self.units
return ExcellonFile(self.statements, self.tools, self.hits,
@ -428,14 +451,13 @@ class ExcellonParser(object):
zeros=self.zeros, notation=self.notation)
def detect_excellon_format(filename):
def detect_excellon_format(data=None, filename=None):
""" Detect excellon file decimal format and zero-suppression settings.
Parameters
----------
filename : string
Name of the file to parse. This does not check if the file is actually
an Excellon file, so do that before calling this.
data : string
String containing contents of Excellon file.
Returns
-------
@ -449,10 +471,16 @@ def detect_excellon_format(filename):
detected_format = None
zeros_options = ('leading', 'trailing', )
format_options = ((2, 4), (2, 5), (3, 3),)
if data is None and filename is None:
raise ValueError('Either data or filename arguments must be provided')
if data is None:
with open(filename, 'r') as f:
data = f.read()
# Check for obvious clues:
p = ExcellonParser()
p.parse(filename)
p.parse_raw(data)
# Get zero_suppression from a unit statement
zero_statements = [stmt.zeros for stmt in p.statements
@ -485,8 +513,8 @@ def detect_excellon_format(filename):
settings = FileSettings(zeros=zeros, format=fmt)
try:
p = ExcellonParser(settings)
p.parse(filename)
size = tuple([t[1] - t[0] for t in p.bounds])
p.parse_raw(data)
size = tuple([t[0] - t[1] for t in p.bounds])
hole_area = 0.0
for hit in p.hits:
tool = hit.tool

Wyświetl plik

@ -172,3 +172,9 @@ class GerberCairoContext(GerberContext):
else:
self.surface.write_to_png(filename)
def dump_svg_str(self):
self.surface.finish()
self.surface_buffer.flush()
return self.surface_buffer.read()

Wyświetl plik

@ -181,3 +181,4 @@ class GerberContext(object):
def _render_test_record(self, primitive, color):
pass

Wyświetl plik

@ -21,6 +21,7 @@
import copy
import json
import re
from cStringIO import StringIO
from .gerber_statements import *
from .primitives import *
@ -43,6 +44,9 @@ def read(filename):
return GerberParser().parse(filename)
def loads(data):
return GerberParser().parse_raw(data)
class GerberFile(CamFile):
""" A class representing a single gerber file
@ -75,7 +79,6 @@ class GerberFile(CamFile):
def __init__(self, statements, settings, primitives, filename=None):
super(GerberFile, self).__init__(statements, settings, primitives, filename)
@property
def comments(self):
return [comment.comment for comment in self.statements
@ -205,12 +208,14 @@ class GerberParser(object):
self.quadrant_mode = 'multi-quadrant'
self.step_and_repeat = (1, 1, 0, 0)
def parse(self, filename):
fp = open(filename, "r")
data = fp.readlines()
with open(filename, "r") as fp:
data = fp.read()
return self.parse_raw(data, filename=None)
for stmt in self._parse(data):
def parse_raw(self, data, filename=None):
lines = [line for line in StringIO(data)]
for stmt in self._parse(lines):
self.evaluate(stmt)
self.statements.append(stmt)
@ -225,10 +230,10 @@ class GerberParser(object):
return json.dumps(stmts)
def dump_str(self):
s = ""
string = ""
for stmt in self.statements:
s += str(stmt) + "\n"
return s
string += str(stmt) + "\n"
return string
def _parse(self, data):
oldline = ''
@ -404,7 +409,6 @@ class GerberParser(object):
else:
raise Exception("Invalid statement to evaluate")
def _define_aperture(self, d, shape, modifiers):
aperture = None
if shape == 'C':
@ -490,7 +494,7 @@ class GerberParser(object):
self.current_region = [Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units),]
else:
self.current_region.append(Arc(start, end, center, self.direction, self.apertures[self.aperture], level_polarity=self.level_polarity, units=self.settings.units))
elif self.op == "D02":
pass

Wyświetl plik

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
from ..common import read
from ..common import read, loads
from ..excellon import ExcellonFile
from ..rs274x import GerberFile
from .tests import *
@ -23,9 +23,20 @@ def test_file_type_detection():
assert_true(isinstance(ncdrill, ExcellonFile))
assert_true(isinstance(top_copper, GerberFile))
def test_load_from_string():
with open(NCDRILL_FILE, 'r') as f:
ncdrill = loads(f.read())
with open(TOP_COPPER_FILE, 'r') as f:
top_copper = loads(f.read())
assert_true(isinstance(ncdrill, ExcellonFile))
assert_true(isinstance(top_copper, GerberFile))
def test_file_type_validation():
""" Test file format validation
"""
assert_raises(TypeError, read, 'LICENSE')

Wyświetl plik

@ -11,41 +11,51 @@ from .tests import *
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
'resources/ncdrill.DRD')
'resources/ncdrill.DRD')
def test_format_detection():
""" Test file type detection
"""
settings = detect_excellon_format(NCDRILL_FILE)
with open(NCDRILL_FILE) as f:
data = f.read()
settings = detect_excellon_format(data)
assert_equal(settings['format'], (2, 4))
assert_equal(settings['zeros'], 'trailing')
settings = detect_excellon_format(filename=NCDRILL_FILE)
assert_equal(settings['format'], (2, 4))
assert_equal(settings['zeros'], 'trailing')
def test_read():
ncdrill = read(NCDRILL_FILE)
assert(isinstance(ncdrill, ExcellonFile))
def test_write():
ncdrill = read(NCDRILL_FILE)
ncdrill.write('test.ncd')
with open(NCDRILL_FILE) as src:
srclines = src.readlines()
srclines = src.readlines()
with open('test.ncd') as res:
for idx, line in enumerate(res):
assert_equal(line.strip(), srclines[idx].strip())
for idx, line in enumerate(res):
assert_equal(line.strip(), srclines[idx].strip())
os.remove('test.ncd')
def test_read_settings():
ncdrill = read(NCDRILL_FILE)
assert_equal(ncdrill.settings['format'], (2, 4))
assert_equal(ncdrill.settings['zeros'], 'trailing')
def test_bounds():
ncdrill = read(NCDRILL_FILE)
xbound, ybound = ncdrill.bounds
assert_array_almost_equal(xbound, (0.1300, 2.1430))
assert_array_almost_equal(ybound, (0.3946, 1.7164))
def test_report():
ncdrill = read(NCDRILL_FILE)
@ -57,9 +67,7 @@ def test_conversion():
ncdrill_inch = copy.deepcopy(ncdrill)
ncdrill.to_metric()
assert_equal(ncdrill.settings.units, 'metric')
inch_primitives = ncdrill_inch.primitives
for tool in iter(ncdrill_inch.tools.values()):
tool.to_metric()
for primitive in inch_primitives:
@ -80,26 +88,31 @@ def test_parser_hole_count():
p.parse(NCDRILL_FILE)
assert_equal(p.hole_count, 36)
def test_parser_hole_sizes():
settings = FileSettings(**detect_excellon_format(NCDRILL_FILE))
p = ExcellonParser(settings)
p.parse(NCDRILL_FILE)
assert_equal(p.hole_sizes, [0.0236, 0.0354, 0.04, 0.126, 0.128])
def test_parse_whitespace():
p = ExcellonParser(FileSettings())
assert_equal(p._parse(' '), None)
def test_parse_comment():
p = ExcellonParser(FileSettings())
p._parse(';A comment')
assert_equal(p.statements[0].comment, 'A comment')
def test_parse_format_comment():
p = ExcellonParser(FileSettings())
p._parse('; FILE_FORMAT=9:9 ')
assert_equal(p.format, (9, 9))
def test_parse_header():
p = ExcellonParser(FileSettings())
p._parse('M48 ')
@ -107,6 +120,7 @@ def test_parse_header():
p._parse('M95 ')
assert_equal(p.state, 'DRILL')
def test_parse_rout():
p = ExcellonParser(FileSettings())
p._parse('G00 ')
@ -114,6 +128,7 @@ def test_parse_rout():
p._parse('G05 ')
assert_equal(p.state, 'DRILL')
def test_parse_version():
p = ExcellonParser(FileSettings())
p._parse('VER,1 ')
@ -121,6 +136,7 @@ def test_parse_version():
p._parse('VER,2 ')
assert_equal(p.statements[1].version, 2)
def test_parse_format():
p = ExcellonParser(FileSettings())
p._parse('FMAT,1 ')
@ -128,6 +144,7 @@ def test_parse_format():
p._parse('FMAT,2 ')
assert_equal(p.statements[1].format, 2)
def test_parse_units():
settings = FileSettings(units='inch', zeros='trailing')
p = ExcellonParser(settings)
@ -138,6 +155,7 @@ def test_parse_units():
assert_equal(p.units, 'metric')
assert_equal(p.zeros, 'leading')
def test_parse_incremental_mode():
settings = FileSettings(units='inch', zeros='trailing')
p = ExcellonParser(settings)
@ -147,6 +165,7 @@ def test_parse_incremental_mode():
p._parse('ICI,OFF ')
assert_equal(p.notation, 'absolute')
def test_parse_absolute_mode():
settings = FileSettings(units='inch', zeros='trailing')
p = ExcellonParser(settings)
@ -156,18 +175,21 @@ def test_parse_absolute_mode():
p._parse('G90 ')
assert_equal(p.notation, 'absolute')
def test_parse_repeat_hole():
p = ExcellonParser(FileSettings())
p.active_tool = ExcellonTool(FileSettings(), number=8)
p._parse('R03X1.5Y1.5')
assert_equal(p.statements[0].count, 3)
def test_parse_incremental_position():
p = ExcellonParser(FileSettings(notation='incremental'))
p._parse('X01Y01')
p._parse('X01Y01')
assert_equal(p.pos, [2.,2.])
def test_parse_unknown():
p = ExcellonParser(FileSettings())
p._parse('Not A Valid Statement')

Wyświetl plik

@ -201,30 +201,20 @@ def decimal_string(value, precision=6, padding=False):
return int(floatstr)
def detect_file_format(filename):
def detect_file_format(data):
""" Determine format of a file
Parameters
----------
filename : string
Filename of the file to read.
data : string
string containing file data.
Returns
-------
format : string
File format. either 'excellon' or 'rs274x'
File format. 'excellon' or 'rs274x' or 'unknown'
"""
# Read the first 20 lines (if possible)
lines = []
with open(filename, 'r') as f:
try:
for i in range(20):
lines.append(f.readline())
except StopIteration:
pass
# Look for
lines = data.split('\n')
for line in lines:
if 'M48' in line:
return 'excellon'