From dd63b169f177389602e17bc6ced53bd0f1ba0de3 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 10 Oct 2015 16:51:21 -0400 Subject: 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 --- gerber/__init__.py | 2 +- gerber/common.py | 36 ++++++++++++++++++++++++++---- gerber/excellon.py | 50 ++++++++++++++++++++++++++++++++---------- gerber/render/cairo_backend.py | 6 +++++ gerber/render/render.py | 1 + gerber/rs274x.py | 24 +++++++++++--------- gerber/tests/test_common.py | 13 ++++++++++- gerber/tests/test_excellon.py | 38 +++++++++++++++++++++++++------- gerber/utils.py | 20 +++++------------ 9 files changed, 140 insertions(+), 50 deletions(-) (limited to 'gerber') diff --git a/gerber/__init__.py b/gerber/__init__.py index 1a11159..b5a9014 100644 --- a/gerber/__init__.py +++ b/gerber/__init__.py @@ -23,4 +23,4 @@ gerber-tools provides utilities for working with Gerber (RS-274X) and Excellon files in python. """ -from .common import read \ No newline at end of file +from .common import read, loads \ No newline at end of file diff --git a/gerber/common.py b/gerber/common.py index 78da2cd..50ba728 100644 --- a/gerber/common.py +++ b/gerber/common.py @@ -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') + + diff --git a/gerber/excellon.py b/gerber/excellon.py index d89b349..ba8573d 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -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 diff --git a/gerber/render/cairo_backend.py b/gerber/render/cairo_backend.py index a97e552..345f331 100644 --- a/gerber/render/cairo_backend.py +++ b/gerber/render/cairo_backend.py @@ -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() + \ No newline at end of file diff --git a/gerber/render/render.py b/gerber/render/render.py index 124e743..8f49796 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -181,3 +181,4 @@ class GerberContext(object): def _render_test_record(self, primitive, color): pass + diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 1df3646..000f7a1 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -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 diff --git a/gerber/tests/test_common.py b/gerber/tests/test_common.py index 76e3991..0ba4b68 100644 --- a/gerber/tests/test_common.py +++ b/gerber/tests/test_common.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Author: Hamilton Kibbe -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') + diff --git a/gerber/tests/test_excellon.py b/gerber/tests/test_excellon.py index 006277d..b821649 100644 --- a/gerber/tests/test_excellon.py +++ b/gerber/tests/test_excellon.py @@ -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') diff --git a/gerber/utils.py b/gerber/utils.py index df26516..1c0af52 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -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' -- cgit From 10d9028e1fdf7431baee73c7f1474d2134bac5fa Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 10 Oct 2015 17:02:45 -0400 Subject: Python 3 fix --- gerber/excellon.py | 6 +++++- gerber/rs274x.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) (limited to 'gerber') diff --git a/gerber/excellon.py b/gerber/excellon.py index ba8573d..7333a98 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -25,7 +25,11 @@ This module provides Excellon file classes and parsing utilities import math import operator -from cStringIO import StringIO + +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO from .excellon_statements import * from .cam import CamFile, FileSettings diff --git a/gerber/rs274x.py b/gerber/rs274x.py index 000f7a1..210b590 100644 --- a/gerber/rs274x.py +++ b/gerber/rs274x.py @@ -21,8 +21,12 @@ import copy import json import re -from cStringIO import StringIO +try: + from cStringIO import StringIO +except(ImportError): + from io import StringIO + from .gerber_statements import * from .primitives import * from .cam import CamFile, FileSettings -- cgit