kopia lustrzana https://gitlab.com/gerbolyze/gerbonara
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 stringrefactor
rodzic
b81c9d4bf9
commit
dd63b169f1
|
@ -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'))
|
||||
|
|
|
@ -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
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -181,3 +181,4 @@ class GerberContext(object):
|
|||
|
||||
def _render_test_record(self, primitive, color):
|
||||
pass
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'
|
||||
|
|
Ładowanie…
Reference in New Issue