From 76c03a55c91addff71339d80cf17560926f1580b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 20:36:38 -0400 Subject: Working region fills and level polarity. Renders Altium-generated gerbers like a champ! --- gerber/gerber_statements.py | 16 ++++++++-------- gerber/render/render.py | 23 +++++++++++++++-------- gerber/render/svgwrite_backend.py | 22 ++++++++++++++++++++-- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index 9072b58..a22eae2 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -17,9 +17,9 @@ __all__ = ['FSParamStmt', 'MOParamStmt', 'IPParamStmt', 'OFParamStmt', class Statement(object): """ Gerber statement Base class - + The statement class provides a type attribute. - + Parameters ---------- type : string @@ -27,7 +27,7 @@ class Statement(object): Attributes ---------- - type : string + type : string String identifying the statement type. """ def __init__(self, stype): @@ -45,9 +45,9 @@ class Statement(object): class ParamStmt(Statement): """ Gerber parameter statement Base class - + The parameter statement class provides a parameter type attribute. - + Parameters ---------- param : string @@ -55,7 +55,7 @@ class ParamStmt(Statement): Attributes ---------- - param : string + param : string Parameter type code """ def __init__(self, param): @@ -260,7 +260,7 @@ class LPParamStmt(ParamStmt): @classmethod def from_dict(cls, stmt_dict): - param = stmt_dict.get('lp') + param = stmt_dict['param'] lp = 'clear' if stmt_dict.get('lp') == 'C' else 'dark' return cls(param, lp) @@ -667,6 +667,6 @@ class UnknownStmt(Statement): def __init__(self, line): Statement.__init__(self, "UNKNOWN") self.line = line - + def to_gerber(self): return self.line diff --git a/gerber/render/render.py b/gerber/render/render.py index e76aed1..48a53f8 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -96,7 +96,7 @@ class GerberContext(object): self.level_polarity = 'dark' self.region_mode = 'off' self.quadrant_mode = 'multi-quadrant' - + self.step_and_repeat = (1, 1, 0, 0) self.color = (0.7215, 0.451, 0.200) self.drill_color = (0.25, 0.25, 0.25) self.background_color = (0.0, 0.0, 0.0) @@ -415,6 +415,12 @@ class GerberContext(object): """ pass + def region_contour(self, x, y): + pass + + def fill_region(self): + pass + def evaluate(self, stmt): """ Evaluate Gerber statement and update image accordingly. @@ -450,7 +456,7 @@ class GerberContext(object): def _evaluate_mode(self, stmt): if stmt.type == 'RegionMode': if self.region_mode == 'on' and stmt.mode == 'off': - self._fill_region() + self.fill_region() self.region_mode = stmt.mode elif stmt.type == 'QuadrantMode': self.quadrant_mode = stmt.mode @@ -460,11 +466,11 @@ class GerberContext(object): self.set_coord_format(stmt.zero_suppression, stmt.format, stmt.notation) self.set_coord_notation(stmt.notation) - elif stmt.param == "MO:": + elif stmt.param == "MO": self.set_coord_unit(stmt.mode) - elif stmt.param == "IP:": + elif stmt.param == "IP": self.set_image_polarity(stmt.ip) - elif stmt.param == "LP:": + elif stmt.param == "LP": self.set_level_polarity(stmt.lp) elif stmt.param == "AD": self.define_aperture(stmt.d, stmt.shape, stmt.modifiers) @@ -477,7 +483,10 @@ class GerberContext(object): self.direction = ('clockwise' if stmt.function in ('G02', 'G2') else 'counterclockwise') if stmt.op == "D01": - self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) + if self.region_mode == 'on': + self.region_contour(stmt.x, stmt.y) + else: + self.stroke(stmt.x, stmt.y, stmt.i, stmt.j) elif stmt.op == "D02": self.move(stmt.x, stmt.y) elif stmt.op == "D03": @@ -486,5 +495,3 @@ class GerberContext(object): def _evaluate_aperture(self, stmt): self.set_aperture(stmt.d) - def _fill_region(self): - pass diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 7570c84..886b4f8 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -118,6 +118,7 @@ class GerberSvgContext(GerberContext): self.apertures = {} self.dwg = svgwrite.Drawing() self.background = False + self.region_path = None def set_bounds(self, bounds): xbounds, ybounds = bounds @@ -125,7 +126,7 @@ class GerberSvgContext(GerberContext): if not self.background: self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), - size=size, fill="black")) + size=size, fill=convert_color(self.background_color))) self.background = True def define_aperture(self, d, shape, modifiers): @@ -173,7 +174,8 @@ class GerberSvgContext(GerberContext): ap = self.apertures.get(self.aperture, None) if ap is None: return - color = (convert_color(self.color) if self.level_polarity == 'dark' + + color = (convert_color(self.color) if self.level_polarity == 'dark' else convert_color(self.background_color)) for shape in ap.flash(self, x, y, color): self.dwg.add(shape) @@ -185,5 +187,21 @@ class GerberSvgContext(GerberContext): fill=convert_color(self.drill_color)) self.dwg.add(hit) + def region_contour(self, x, y): + super(GerberSvgContext, self).region_contour(x, y) + x, y = self.resolve(x, y) + color = (convert_color(self.color) if self.level_polarity == 'dark' + else convert_color(self.background_color)) + if self.region_path is None: + self.region_path = self.dwg.path(d = 'M %f, %f' % + (self.x*SCALE, -self.y*SCALE), + fill = color, stroke = 'none') + self.region_path.push('L %f, %f' % (x*SCALE, -y*SCALE)) + self.move(x, y, resolve=False) + + def fill_region(self): + self.dwg.add(self.region_path) + self.region_path = None + def dump(self, filename): self.dwg.saveas(filename) -- cgit From ae3bbff8b0849e0b49dc139396d7f8c57334a7b8 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Fri, 10 Oct 2014 23:07:51 -0400 Subject: Added excellon format detection --- gerber/cam.py | 124 ++++++++++++++++++++++++++ gerber/cnc.py | 119 ------------------------- gerber/excellon.py | 155 +++++++++++++++++++++++++++++++-- gerber/gerber.py | 4 +- gerber/gerber_statements.py | 7 +- gerber/tests/test_cam.py | 50 +++++++++++ gerber/tests/test_cnc.py | 50 ----------- gerber/tests/test_gerber_statements.py | 76 ++++++++++++---- 8 files changed, 392 insertions(+), 193 deletions(-) create mode 100644 gerber/cam.py delete mode 100644 gerber/cnc.py create mode 100644 gerber/tests/test_cam.py delete mode 100644 gerber/tests/test_cnc.py diff --git a/gerber/cam.py b/gerber/cam.py new file mode 100644 index 0000000..e7a49d1 --- /dev/null +++ b/gerber/cam.py @@ -0,0 +1,124 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# copyright 2014 Hamilton Kibbe +# +# 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. +""" +CAM File +============ +**AM file classes** + +This module provides common base classes for Excellon/Gerber CNC files +""" + + +class FileSettings(object): + """ CAM File Settings + + Provides a common representation of gerber/excellon file settings + """ + def __init__(self, notation='absolute', units='inch', + zero_suppression='trailing', format=(2, 5)): + if notation not in ['absolute', 'incremental']: + raise ValueError('Notation must be either absolute or incremental') + self.notation = notation + + if units not in ['inch', 'metric']: + raise ValueError('Units must be either inch or metric') + self.units = units + + if zero_suppression not in ['leading', 'trailing']: + raise ValueError('Zero suppression must be either leading or \ + trailling') + self.zero_suppression = zero_suppression + + if len(format) != 2: + raise ValueError('Format must be a tuple(n=2) of integers') + self.format = format + + def __getitem__(self, key): + if key == 'notation': + return self.notation + elif key == 'units': + return self.units + elif key == 'zero_suppression': + return self.zero_suppression + elif key == 'format': + return self.format + else: + raise KeyError() + + +class CamFile(object): + """ Base class for Gerber/Excellon files. + + Provides a common set of settings parameters. + + Parameters + ---------- + settings : FileSettings + The current file configuration. + + filename : string + Name of the file that this CamFile represents. + + layer_name : string + Name of the PCB layer that the file represents + + Attributes + ---------- + settings : FileSettings + File settings as a FileSettings object + + notation : string + File notation setting. May be either 'absolute' or 'incremental' + + units : string + File units setting. May be 'inch' or 'metric' + + zero_suppression : string + File zero-suppression setting. May be either 'leading' or 'trailling' + + format : tuple (, ) + File decimal representation format as a tuple of (integer digits, + decimal digits) + """ + + def __init__(self, statements=None, settings=None, filename=None, + layer_name=None): + if settings is not None: + self.notation = settings['notation'] + self.units = settings['units'] + self.zero_suppression = settings['zero_suppression'] + self.format = settings['format'] + else: + self.notation = 'absolute' + self.units = 'inch' + self.zero_suppression = 'trailing' + self.format = (2, 5) + self.statements = statements if statements is not None else [] + self.filename = filename + self.layer_name = layer_name + + @property + def settings(self): + """ File settings + + Returns + ------- + settings : FileSettings (dict-like) + A FileSettings object with the specified configuration. + """ + return FileSettings(self.notation, self.units, self.zero_suppression, + self.format) diff --git a/gerber/cnc.py b/gerber/cnc.py deleted file mode 100644 index d17517a..0000000 --- a/gerber/cnc.py +++ /dev/null @@ -1,119 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# copyright 2014 Hamilton Kibbe -# -# 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. -""" -gerber.cnc -============ -**CNC file classes** - -This module provides common base classes for Excellon/Gerber CNC files -""" - - -class FileSettings(object): - """ CNC File Settings - - Provides a common representation of gerber/excellon file settings - """ - def __init__(self, notation='absolute', units='inch', - zero_suppression='trailing', format=(2, 5)): - if notation not in ['absolute', 'incremental']: - raise ValueError('Notation must be either absolute or incremental') - self.notation = notation - - if units not in ['inch', 'metric']: - raise ValueError('Units must be either inch or metric') - self.units = units - - if zero_suppression not in ['leading', 'trailing']: - raise ValueError('Zero suppression must be either leading or \ - trailling') - self.zero_suppression = zero_suppression - - if len(format) != 2: - raise ValueError('Format must be a tuple(n=2) of integers') - self.format = format - - def __getitem__(self, key): - if key == 'notation': - return self.notation - elif key == 'units': - return self.units - elif key == 'zero_suppression': - return self.zero_suppression - elif key == 'format': - return self.format - else: - raise KeyError() - - -class CncFile(object): - """ Base class for Gerber/Excellon files. - - Provides a common set of settings parameters. - - Parameters - ---------- - settings : FileSettings - The current file configuration. - - filename : string - Name of the file that this CncFile represents. - - Attributes - ---------- - settings : FileSettings - File settings as a FileSettings object - - notation : string - File notation setting. May be either 'absolute' or 'incremental' - - units : string - File units setting. May be 'inch' or 'metric' - - zero_suppression : string - File zero-suppression setting. May be either 'leading' or 'trailling' - - format : tuple (, ) - File decimal representation format as a tuple of (integer digits, - decimal digits) - """ - - def __init__(self, statements=None, settings=None, filename=None): - if settings is not None: - self.notation = settings['notation'] - self.units = settings['units'] - self.zero_suppression = settings['zero_suppression'] - self.format = settings['format'] - else: - self.notation = 'absolute' - self.units = 'inch' - self.zero_suppression = 'trailing' - self.format = (2, 5) - self.statements = statements if statements is not None else [] - self.filename = filename - - @property - def settings(self): - """ File settings - - Returns - ------- - settings : FileSettings (dict-like) - A FileSettings object with the specified configuration. - """ - return FileSettings(self.notation, self.units, self.zero_suppression, - self.format) diff --git a/gerber/excellon.py b/gerber/excellon.py index 4166de6..1a498dc 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -24,16 +24,22 @@ This module provides Excellon file classes and parsing utilities from .excellon_statements import * -from .cnc import CncFile, FileSettings +from .cam import CamFile, FileSettings +import math def read(filename): """ Read data from filename and return an ExcellonFile """ - return ExcellonParser().parse(filename) + detected_settings = detect_excellon_format(filename) + settings = FileSettings(**detected_settings) + zeros = '' + print('Detected %d:%d format with %s zero suppression' % + (settings.format[0], settings.format[1], settings.zero_suppression)) + return ExcellonParser(settings).parse(filename) -class ExcellonFile(CncFile): +class ExcellonFile(CamFile): """ A class representing a single excellon file The ExcellonFile class represents a single excellon file. @@ -83,8 +89,13 @@ class ExcellonFile(CncFile): class ExcellonParser(object): """ Excellon File Parser + + Parameters + ---------- + settings : FileSettings or dict-like + Excellon file settings to use when interpreting the excellon file. """ - def __init__(self): + def __init__(self, settings=None): self.notation = 'absolute' self.units = 'inch' self.zero_suppression = 'trailing' @@ -95,7 +106,38 @@ class ExcellonParser(object): self.hits = [] self.active_tool = None self.pos = [0., 0.] - + if settings is not None: + self.units = settings['units'] + self.zero_suppression = settings['zero_suppression'] + self.notation = settings['notation'] + self.format = settings['format'] + + + @property + def coordinates(self): + return [(stmt.x, stmt.y) for stmt in self.statements if isinstance(stmt, CoordinateStmt)] + + @property + def bounds(self): + xmin = ymin = 100000000000 + xmax = ymax = -100000000000 + for x, y in self.coordinates: + if x is not None: + xmin = x if x < xmin else xmin + xmax = x if x > xmax else xmax + if y is not None: + ymin = y if y < ymin else ymin + ymax = y if y > ymax else ymax + return ((xmin, xmax), (ymin, ymax)) + + @property + def hole_sizes(self): + return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)] + + @property + def hole_count(self): + return len(self.hits) + def parse(self, filename): with open(filename, 'r') as f: for line in f: @@ -194,3 +236,106 @@ class ExcellonParser(object): return FileSettings(units=self.units, format=self.format, zero_suppression=self.zero_suppression, notation=self.notation) + + +def detect_excellon_format(filename): + """ 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. + + Returns + ------- + settings : dict + Detected excellon file settings. Keys are + - `format`: decimal format as tuple (, ) + - `zero_suppression`: zero suppression, 'leading' or 'trailing' + """ + results = {} + detected_zeros = None + detected_format = None + zs_options = ('leading', 'trailing', ) + format_options = ((2, 4), (2, 5), (3, 3),) + + # Check for obvious clues: + p = ExcellonParser() + p.parse(filename) + + # Get zero_suppression from a unit statement + zero_statements = [stmt.zero_suppression for stmt in p.statements + if isinstance(stmt, UnitStmt)] + + # get format from altium comment + format_comment = [stmt.comment for stmt in p.statements + if isinstance(stmt, CommentStmt) + and 'FILE_FORMAT' in stmt.comment] + + detected_format = (tuple([int(val) for val in + format_comment[0].split('=')[1].split(':')]) + if len(format_comment) == 1 else None) + detected_zeros = zero_statements[0] if len(zero_statements) == 1 else None + + # Bail out here if possible + if detected_format is not None and detected_zeros is not None: + return {'format': detected_format, 'zero_suppression': detected_zeros} + + # Only look at remaining options + if detected_format is not None: + format_options = (detected_format,) + if detected_zeros is not None: + zs_options = (detected_zeros,) + + # Brute force all remaining options, and pick the best looking one... + for zs in zs_options: + for fmt in format_options: + key = (fmt, zs) + settings = FileSettings(zero_suppression=zs, format=fmt) + try: + p = ExcellonParser(settings) + p.parse(filename) + size = tuple([t[1] - t[0] for t in p.bounds]) + hole_area = 0.0 + for hit in p.hits: + tool = hit[0] + hole_area += math.pow(math.pi * tool.diameter, 2) + results[key] = (size, p.hole_count, hole_area) + except: + pass + + # See if any of the dimensions are left with only a single option + formats = set(key[0] for key in results.iterkeys()) + zeros = set(key[1] for key in results.iterkeys()) + if len(formats) == 1: + detected_format = formats.pop() + if len(zeros) == 1: + detected_zeros = zeros.pop() + + # Bail out here if we got everything.... + if detected_format is not None and detected_zeros is not None: + return {'format': detected_format, 'zero_suppression': detected_zeros} + + # Otherwise score each option and pick the best candidate + else: + scores = {} + for key in results.keys(): + size, count, diameter = results[key] + scores[key] = _layer_size_score(size, count, diameter) + minscore = min(scores.values()) + for key in scores.iterkeys(): + if scores[key] == minscore: + return {'format': key[0], 'zero_suppression': key[1]} + + +def _layer_size_score(size, hole_count, hole_area): + """ Heuristic used for determining the correct file number interpretation. + Lower is better. + """ + board_area = size[0] * size[1] + hole_percentage = hole_area / board_area + hole_score = (hole_percentage - 0.25) ** 2 + size_score = (board_area - 8) **2 + return hole_score * size_score + \ No newline at end of file diff --git a/gerber/gerber.py b/gerber/gerber.py index 4ce261d..215b970 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -27,7 +27,7 @@ This module provides an RS-274-X class and parser import re import json from .gerber_statements import * -from .cnc import CncFile, FileSettings +from .cam import CamFile, FileSettings @@ -38,7 +38,7 @@ def read(filename): return GerberParser().parse(filename) -class GerberFile(CncFile): +class GerberFile(CamFile): """ A class representing a single gerber file The GerberFile class represents a single gerber file. diff --git a/gerber/gerber_statements.py b/gerber/gerber_statements.py index a22eae2..218074f 100644 --- a/gerber/gerber_statements.py +++ b/gerber/gerber_statements.py @@ -133,7 +133,12 @@ class MOParamStmt(ParamStmt): @classmethod def from_dict(cls, stmt_dict): param = stmt_dict.get('param') - mo = 'inch' if stmt_dict.get('mo') == 'IN' else 'metric' + if stmt_dict.get('mo').lower() == 'in': + mo = 'inch' + elif stmt_dict.get('mo').lower() == 'mm': + mo = 'metric' + else: + mo = None return cls(param, mo) def __init__(self, param, mo): diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py new file mode 100644 index 0000000..4af1984 --- /dev/null +++ b/gerber/tests/test_cam.py @@ -0,0 +1,50 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +# Author: Hamilton Kibbe + +from ..cam import CamFile, FileSettings +from tests import * + + +def test_smoke_filesettings(): + """ Smoke test FileSettings class + """ + fs = FileSettings() + + +def test_filesettings_defaults(): + """ Test FileSettings default values + """ + fs = FileSettings() + assert_equal(fs.format, (2, 5)) + assert_equal(fs.notation, 'absolute') + assert_equal(fs.zero_suppression, 'trailing') + assert_equal(fs.units, 'inch') + + +def test_filesettings_dict(): + """ Test FileSettings Dict + """ + fs = FileSettings() + assert_equal(fs['format'], (2, 5)) + assert_equal(fs['notation'], 'absolute') + assert_equal(fs['zero_suppression'], 'trailing') + assert_equal(fs['units'], 'inch') + + +def test_filesettings_assign(): + """ Test FileSettings attribute assignment + """ + fs = FileSettings() + fs.units = 'test' + fs.notation = 'test' + fs.zero_suppression = 'test' + fs.format = 'test' + assert_equal(fs.units, 'test') + assert_equal(fs.notation, 'test') + assert_equal(fs.zero_suppression, 'test') + assert_equal(fs.format, 'test') + +def test_smoke_camfile(): + cf = CamFile diff --git a/gerber/tests/test_cnc.py b/gerber/tests/test_cnc.py deleted file mode 100644 index ace047e..0000000 --- a/gerber/tests/test_cnc.py +++ /dev/null @@ -1,50 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- - -# Author: Hamilton Kibbe - -from ..cnc import CncFile, FileSettings -from tests import * - - -def test_smoke_filesettings(): - """ Smoke test FileSettings class - """ - fs = FileSettings() - - -def test_filesettings_defaults(): - """ Test FileSettings default values - """ - fs = FileSettings() - assert_equal(fs.format, (2, 5)) - assert_equal(fs.notation, 'absolute') - assert_equal(fs.zero_suppression, 'trailing') - assert_equal(fs.units, 'inch') - - -def test_filesettings_dict(): - """ Test FileSettings Dict - """ - fs = FileSettings() - assert_equal(fs['format'], (2, 5)) - assert_equal(fs['notation'], 'absolute') - assert_equal(fs['zero_suppression'], 'trailing') - assert_equal(fs['units'], 'inch') - - -def test_filesettings_assign(): - """ Test FileSettings attribute assignment - """ - fs = FileSettings() - fs.units = 'test' - fs.notation = 'test' - fs.zero_suppression = 'test' - fs.format = 'test' - assert_equal(fs.units, 'test') - assert_equal(fs.notation, 'test') - assert_equal(fs.zero_suppression, 'test') - assert_equal(fs.format, 'test') - - def test_smoke_cncfile(): - pass diff --git a/gerber/tests/test_gerber_statements.py b/gerber/tests/test_gerber_statements.py index 9e73fd4..a463c9d 100644 --- a/gerber/tests/test_gerber_statements.py +++ b/gerber/tests/test_gerber_statements.py @@ -8,7 +8,7 @@ from ..gerber_statements import * def test_FSParamStmt_factory(): - """ Test FSParamStruct factory correctly handles parameters + """ Test FSParamStruct factory """ stmt = {'param': 'FS', 'zero': 'L', 'notation': 'A', 'x': '27'} fs = FSParamStmt.from_dict(stmt) @@ -24,6 +24,18 @@ def test_FSParamStmt_factory(): assert_equal(fs.notation, 'incremental') assert_equal(fs.format, (2, 7)) +def test_FSParamStmt(): + """ Test FSParamStmt initialization + """ + param = 'FS' + zeros = 'trailing' + notation = 'absolute' + fmt = (2, 5) + stmt = FSParamStmt(param, zeros, notation, fmt) + assert_equal(stmt.param, param) + assert_equal(stmt.zero_suppression, zeros) + assert_equal(stmt.notation, notation) + assert_equal(stmt.format, fmt) def test_FSParamStmt_dump(): """ Test FSParamStmt to_gerber() @@ -38,17 +50,31 @@ def test_FSParamStmt_dump(): def test_MOParamStmt_factory(): - """ Test MOParamStruct factory correctly handles parameters + """ Test MOParamStruct factory """ - stmt = {'param': 'MO', 'mo': 'IN'} - mo = MOParamStmt.from_dict(stmt) - assert_equal(mo.param, 'MO') - assert_equal(mo.mode, 'inch') + stmts = [{'param': 'MO', 'mo': 'IN'}, {'param': 'MO', 'mo': 'in'}, ] + for stmt in stmts: + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'inch') + + stmts = [{'param': 'MO', 'mo': 'MM'}, {'param': 'MO', 'mo': 'mm'}, ] + for stmt in stmts: + mo = MOParamStmt.from_dict(stmt) + assert_equal(mo.param, 'MO') + assert_equal(mo.mode, 'metric') + +def test_MOParamStmt(): + """ Test MOParamStmt initialization + """ + param = 'MO' + mode = 'inch' + stmt = MOParamStmt(param, mode) + assert_equal(stmt.param, param) - stmt = {'param': 'MO', 'mo': 'MM'} - mo = MOParamStmt.from_dict(stmt) - assert_equal(mo.param, 'MO') - assert_equal(mo.mode, 'metric') + for mode in ['inch', 'metric']: + stmt = MOParamStmt(param, mode) + assert_equal(stmt.mode, mode) def test_MOParamStmt_dump(): @@ -64,7 +90,7 @@ def test_MOParamStmt_dump(): def test_IPParamStmt_factory(): - """ Test IPParamStruct factory correctly handles parameters + """ Test IPParamStruct factory """ stmt = {'param': 'IP', 'ip': 'POS'} ip = IPParamStmt.from_dict(stmt) @@ -74,6 +100,15 @@ def test_IPParamStmt_factory(): ip = IPParamStmt.from_dict(stmt) assert_equal(ip.ip, 'negative') +def test_IPParamStmt(): + """ Test IPParamStmt initialization + """ + param = 'IP' + for ip in ['positive', 'negative']: + stmt = IPParamStmt(param, ip) + assert_equal(stmt.param, param) + assert_equal(stmt.ip, ip) + def test_IPParamStmt_dump(): """ Test IPParamStmt to_gerber() @@ -88,14 +123,23 @@ def test_IPParamStmt_dump(): def test_OFParamStmt_factory(): - """ Test OFParamStmt factory correctly handles parameters + """ Test OFParamStmt factory """ stmt = {'param': 'OF', 'a': '0.1234567', 'b': '0.1234567'} of = OFParamStmt.from_dict(stmt) assert_equal(of.a, 0.1234567) assert_equal(of.b, 0.1234567) - +def test_OFParamStmt(): + """ Test IPParamStmt initialization + """ + param = 'OF' + for val in [0.0, -3.4567]: + stmt = OFParamStmt(param, val, val) + assert_equal(stmt.param, param) + assert_equal(stmt.a, val) + assert_equal(stmt.b, val) + def test_OFParamStmt_dump(): """ Test OFParamStmt to_gerber() """ @@ -105,7 +149,7 @@ def test_OFParamStmt_dump(): def test_LPParamStmt_factory(): - """ Test LPParamStmt factory correctly handles parameters + """ Test LPParamStmt factory """ stmt = {'param': 'LP', 'lp': 'C'} lp = LPParamStmt.from_dict(stmt) @@ -128,7 +172,7 @@ def test_LPParamStmt_dump(): def test_INParamStmt_factory(): - """ Test INParamStmt factory correctly handles parameters + """ Test INParamStmt factory """ stmt = {'param': 'IN', 'name': 'test'} inp = INParamStmt.from_dict(stmt) @@ -143,7 +187,7 @@ def test_INParamStmt_dump(): def test_LNParamStmt_factory(): - """ Test LNParamStmt factory correctly handles parameters + """ Test LNParamStmt factory """ stmt = {'param': 'LN', 'name': 'test'} lnp = LNParamStmt.from_dict(stmt) -- cgit From 62c689be172a7a06d76fd4b69c3443f3ec053765 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 11 Oct 2014 13:12:21 -0400 Subject: Doc update --- README.md | 5 + doc/source/conf.py | 1 + doc/source/documentation/excellon.rst | 42 + doc/source/documentation/gerber.rst | 36 + doc/source/documentation/index.rst | 11 + doc/source/documentation/render.rst | 11 + doc/source/index.rst | 29 +- doc/source/intro.rst | 19 + examples/board.html | 5128 --------------------------------- examples/board.png | Bin 339284 -> 0 bytes examples/board.svg | 5128 --------------------------------- examples/composite_bottom.png | Bin 0 -> 145973 bytes examples/composite_top.png | Bin 0 -> 306805 bytes examples/silk.png | Bin 134995 -> 0 bytes examples/silk.svg | 2852 ------------------ examples/top.png | Bin 216175 -> 0 bytes examples/top.svg | 2278 --------------- gerber/__main__.py | 1 + gerber/excellon.py | 28 +- gerber/gerber.py | 25 +- 20 files changed, 171 insertions(+), 15423 deletions(-) create mode 100644 doc/source/documentation/excellon.rst create mode 100644 doc/source/documentation/gerber.rst create mode 100644 doc/source/documentation/index.rst create mode 100644 doc/source/documentation/render.rst create mode 100644 doc/source/intro.rst delete mode 100644 examples/board.html delete mode 100644 examples/board.png delete mode 100644 examples/board.svg create mode 100644 examples/composite_bottom.png create mode 100644 examples/composite_top.png delete mode 100644 examples/silk.png delete mode 100644 examples/silk.svg delete mode 100644 examples/top.png delete mode 100644 examples/top.svg diff --git a/README.md b/README.md index 1821c0a..ca30576 100644 --- a/README.md +++ b/README.md @@ -20,3 +20,8 @@ Example: # Create SVG image top_copper.render(ctx) nc_drill.render(ctx, 'composite.svg') + + +Rendering: +![Composite Top Image](examples/composite_top.png) +![Composite Bottom Image](examples/composite_bottom.png) \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index 0a3cfc1..ac0fdf7 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -30,6 +30,7 @@ sys.path.insert(0, os.path.abspath('../../')) # ones. extensions = [ 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', 'numpydoc', ] diff --git a/doc/source/documentation/excellon.rst b/doc/source/documentation/excellon.rst new file mode 100644 index 0000000..7ac3b39 --- /dev/null +++ b/doc/source/documentation/excellon.rst @@ -0,0 +1,42 @@ +:mod:`excellon` --- Excellon file handling +============================================== + +.. module:: excellon + :synopsis: Functions and classes for handling Excellon files +.. sectionauthor:: Hamilton Kibbe + + +The Excellon format is the most common format for exporting PCB drill +information. The Excellon format is used to program CNC drilling macines for +drilling holes in PCBs. As such, excellon files are sometimes refererred to as +NC-drill files. The Excellon format reference is available +`here `_. The :mod:`excellon` +submodule implements calsses to read and write excellon files without having +to know the precise details of the format. + +The :mod:`excellon` submodule's :func:`read` function serves as a +simple interface for parsing excellon files. The :class:`ExcellonFile` class +stores all the information contained in an Excellon file allowing the file to +be analyzed, modified, and updated. The :class:`ExcellonParser` class is used +in the background for parsing RS-274X files. + +.. _excellon-contents: + +Functions +--------- +The :mod:`excellon` module defines the following functions: + +.. autofunction:: gerber.excellon.read + + +Classes +------- +The :mod:`excellon` module defines the following classes: + +.. autoclass:: gerber.excellon.ExcellonFile + :members: + + +.. autoclass:: gerber.excellon.ExcellonParser + :members: + \ No newline at end of file diff --git a/doc/source/documentation/gerber.rst b/doc/source/documentation/gerber.rst new file mode 100644 index 0000000..78870a9 --- /dev/null +++ b/doc/source/documentation/gerber.rst @@ -0,0 +1,36 @@ +:mod:`gerber` --- RS-274X file handling +============================================== + +.. module:: gerber + :synopsis: Functions and classes for handling RS-274X files +.. sectionauthor:: Hamilton Kibbe + + +The RS-274X (Gerber) format is the most common format for exporting PCB +artwork. The Specification is published by Ucamco and is available +`here `_. +The :mod:`gerber` submodule implements calsses to read and write +RS-274X files without having to know the precise details of the format. + +The :mod:`gerber` submodule's :func:`read` function serves as a +simple interface for parsing gerber files. The :class:`GerberFile` class +stores all the information contained in a gerber file allowing the file to be +analyzed, modified, and updated. The :class:`GerberParser` class is used in +the background for parsing RS-274X files. + +.. _gerber-contents: +Functions +--------- +The :mod:`gerber` module defines the following functions: + +.. autofunction:: gerber.gerber.read + +Classes +------- +The :mod:`gerber` module defines the following classes: + +.. autoclass:: gerber.gerber.GerberFile + :members: + +.. autoclass:: gerber.gerber.GerberParser + :members: \ No newline at end of file diff --git a/doc/source/documentation/index.rst b/doc/source/documentation/index.rst new file mode 100644 index 0000000..110df87 --- /dev/null +++ b/doc/source/documentation/index.rst @@ -0,0 +1,11 @@ +Gerber Tools Reference +====================== + +.. toctree:: + :maxdepth: 2 + + Gerber (RS-274X) Files + Excellon Files + Rendering + + diff --git a/doc/source/documentation/render.rst b/doc/source/documentation/render.rst new file mode 100644 index 0000000..324ef71 --- /dev/null +++ b/doc/source/documentation/render.rst @@ -0,0 +1,11 @@ +:mod:`render` --- Gerber file Rendering +============================================== + +.. module:: render + :synopsis: Functions and classes for handling Excellon files +.. sectionauthor:: Hamilton Kibbe + +Render Module +------------- +.. automodule:: gerber.render.render + :members: diff --git a/doc/source/index.rst b/doc/source/index.rst index 763d04a..aec8b48 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,37 +3,16 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to Gerber Tools's documentation! +Gerber-Tools! ======================================== Contents: .. toctree:: - :maxdepth: 2 - -.. automodule:: gerber - :members: - -.. automodule:: gerber.gerber - :members: - -.. automodule:: gerber.excellon - :members: - -.. automodule:: gerber.render.render - :members: - -.. automodule:: gerber.gerber_statements - :members: - -.. automodule:: gerber.excellon_statements - :members: - -.. automodule:: gerber.cnc - :members: + :maxdepth: 1 -.. automodule:: gerber.utils - :members: + intro + documentation/index Indices and tables ================== diff --git a/doc/source/intro.rst b/doc/source/intro.rst new file mode 100644 index 0000000..1982fc8 --- /dev/null +++ b/doc/source/intro.rst @@ -0,0 +1,19 @@ +Gerber Tools Intro +================== + +PCB CAM (Gerber) Files +------------ + +PCB design files (artwork) are most often stored in `Gerber` files. This is +a generic term that may refer to `RS-274X (Gerber) `_, +`ODB++ `_, or `Excellon `_ +files. + + +Gerber-Tools +------------ + +The gerber-tools module provides tools for working with and rendering Gerber +and Excellon files. + + diff --git a/examples/board.html b/examples/board.html deleted file mode 100644 index 97a3807..0000000 --- a/examples/board.html +++ /dev/null @@ -1,5128 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/board.png b/examples/board.png deleted file mode 100644 index 948874d..0000000 Binary files a/examples/board.png and /dev/null differ diff --git a/examples/board.svg b/examples/board.svg deleted file mode 100644 index 97a3807..0000000 --- a/examples/board.svg +++ /dev/null @@ -1,5128 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/composite_bottom.png b/examples/composite_bottom.png new file mode 100644 index 0000000..76451a5 Binary files /dev/null and b/examples/composite_bottom.png differ diff --git a/examples/composite_top.png b/examples/composite_top.png new file mode 100644 index 0000000..a68bac8 Binary files /dev/null and b/examples/composite_top.png differ diff --git a/examples/silk.png b/examples/silk.png deleted file mode 100644 index b5fd99f..0000000 Binary files a/examples/silk.png and /dev/null differ diff --git a/examples/silk.svg b/examples/silk.svg deleted file mode 100644 index b84777d..0000000 --- a/examples/silk.svg +++ /dev/null @@ -1,2852 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/top.png b/examples/top.png deleted file mode 100644 index 1ec7e8b..0000000 Binary files a/examples/top.png and /dev/null differ diff --git a/examples/top.svg b/examples/top.svg deleted file mode 100644 index c89449e..0000000 --- a/examples/top.svg +++ /dev/null @@ -1,2278 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gerber/__main__.py b/gerber/__main__.py index ab0f377..1af4c0f 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -30,6 +30,7 @@ if __name__ == '__main__': print "parsing %s" % filename gerberfile = read(filename) gerberfile.render(ctx) + ctx.set_color(tuple([color * 0.4 for color in ctx.color])) print('Saving image to test.svg') ctx.dump('test.svg') diff --git a/gerber/excellon.py b/gerber/excellon.py index 1a498dc..f5d6c29 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -30,6 +30,15 @@ import math def read(filename): """ Read data from filename and return an ExcellonFile + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.excellon.ExcellonFile` + An ExcellonFile created from the specified file. """ detected_settings = detect_excellon_format(filename) settings = FileSettings(**detected_settings) @@ -75,6 +84,14 @@ class ExcellonFile(CamFile): def render(self, ctx, filename=None): """ Generate image of file + + Parameters + ---------- + ctx : :class:`gerber.render.GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, the rendered image will be saved to `filename` """ for tool, pos in self.hits: ctx.drill(pos[0], pos[1], tool.diameter) @@ -89,7 +106,7 @@ class ExcellonFile(CamFile): class ExcellonParser(object): """ Excellon File Parser - + Parameters ---------- settings : FileSettings or dict-like @@ -129,15 +146,15 @@ class ExcellonParser(object): ymin = y if y < ymin else ymin ymax = y if y > ymax else ymax return ((xmin, xmax), (ymin, ymax)) - + @property def hole_sizes(self): return [stmt.diameter for stmt in self.statements if isinstance(stmt, ExcellonTool)] - + @property def hole_count(self): return len(self.hits) - + def parse(self, filename): with open(filename, 'r') as f: for line in f: @@ -316,7 +333,7 @@ def detect_excellon_format(filename): # Bail out here if we got everything.... if detected_format is not None and detected_zeros is not None: return {'format': detected_format, 'zero_suppression': detected_zeros} - + # Otherwise score each option and pick the best candidate else: scores = {} @@ -338,4 +355,3 @@ def _layer_size_score(size, hole_count, hole_area): hole_score = (hole_percentage - 0.25) ** 2 size_score = (board_area - 8) **2 return hole_score * size_score - \ No newline at end of file diff --git a/gerber/gerber.py b/gerber/gerber.py index 215b970..335b443 100644 --- a/gerber/gerber.py +++ b/gerber/gerber.py @@ -15,12 +15,7 @@ # 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. -""" -Gerber File module -================== -**Gerber File module** - -This module provides an RS-274-X class and parser +""" This module provides an RS-274-X class and parser. """ @@ -34,6 +29,16 @@ from .cam import CamFile, FileSettings def read(filename): """ Read data from filename and return a GerberFile + + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.gerber.GerberFile` + A GerberFile created from the specified file. """ return GerberParser().parse(filename) @@ -113,6 +118,14 @@ class GerberFile(CamFile): def render(self, ctx, filename=None): """ Generate image of layer. + + Parameters + ---------- + ctx : :class:`GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, the rendered image will be saved to `filename` """ ctx.set_bounds(self.bounds) for statement in self.statements: -- cgit From d9018da412470053a063b7b28b5e32529fc573f6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 11 Oct 2014 13:14:47 -0400 Subject: Readme update --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ca30576..2695cca 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ gerber-tools Tools to handle Gerber and Excellon files in Python. -Example: - +Useage Example: +--------------- import gerber from gerber.render import GerberSvgContext @@ -22,6 +22,10 @@ Example: nc_drill.render(ctx, 'composite.svg') -Rendering: +Rendering Examples: +------------------- +###Top Composite rendering ![Composite Top Image](examples/composite_top.png) + +###Bottom Composite rendering ![Composite Bottom Image](examples/composite_bottom.png) \ No newline at end of file -- cgit From 8c5c7ec8bbc8a074884ef04b566f9c0ecd6e78bb Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 12 Oct 2014 12:38:40 -0400 Subject: update docs and example images --- TODO.md | 3 - doc/source/conf.py | 2 +- doc/source/documentation/gerber.rst | 36 ---- doc/source/documentation/index.rst | 2 +- doc/source/documentation/rs274x.rst | 37 ++++ examples/composite_bottom.png | Bin 145973 -> 146354 bytes examples/composite_bottom.svg | 2 + examples/composite_top.png | Bin 306805 -> 292317 bytes examples/composite_top.svg | 2 + gerber.md | 73 -------- gerber/__main__.py | 1 - gerber/common.py | 4 +- gerber/excellon.py | 3 +- gerber/gerber.py | 332 ------------------------------------ gerber/render/render.py | 19 ++- gerber/render/svgwrite_backend.py | 7 + gerber/rs274x.py | 327 +++++++++++++++++++++++++++++++++++ 17 files changed, 399 insertions(+), 451 deletions(-) delete mode 100644 TODO.md delete mode 100644 doc/source/documentation/gerber.rst create mode 100644 doc/source/documentation/rs274x.rst create mode 100644 examples/composite_bottom.svg create mode 100644 examples/composite_top.svg delete mode 100644 gerber.md delete mode 100644 gerber/gerber.py create mode 100644 gerber/rs274x.py diff --git a/TODO.md b/TODO.md deleted file mode 100644 index f5aa93b..0000000 --- a/TODO.md +++ /dev/null @@ -1,3 +0,0 @@ -* add command line utilities: gerber svg, gerber transform --rotate --scale --translate, gerber merge --blueprint - -* AM defined apertures \ No newline at end of file diff --git a/doc/source/conf.py b/doc/source/conf.py index ac0fdf7..a118546 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -86,7 +86,7 @@ exclude_patterns = [] # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' diff --git a/doc/source/documentation/gerber.rst b/doc/source/documentation/gerber.rst deleted file mode 100644 index 78870a9..0000000 --- a/doc/source/documentation/gerber.rst +++ /dev/null @@ -1,36 +0,0 @@ -:mod:`gerber` --- RS-274X file handling -============================================== - -.. module:: gerber - :synopsis: Functions and classes for handling RS-274X files -.. sectionauthor:: Hamilton Kibbe - - -The RS-274X (Gerber) format is the most common format for exporting PCB -artwork. The Specification is published by Ucamco and is available -`here `_. -The :mod:`gerber` submodule implements calsses to read and write -RS-274X files without having to know the precise details of the format. - -The :mod:`gerber` submodule's :func:`read` function serves as a -simple interface for parsing gerber files. The :class:`GerberFile` class -stores all the information contained in a gerber file allowing the file to be -analyzed, modified, and updated. The :class:`GerberParser` class is used in -the background for parsing RS-274X files. - -.. _gerber-contents: -Functions ---------- -The :mod:`gerber` module defines the following functions: - -.. autofunction:: gerber.gerber.read - -Classes -------- -The :mod:`gerber` module defines the following classes: - -.. autoclass:: gerber.gerber.GerberFile - :members: - -.. autoclass:: gerber.gerber.GerberParser - :members: \ No newline at end of file diff --git a/doc/source/documentation/index.rst b/doc/source/documentation/index.rst index 110df87..3d8241a 100644 --- a/doc/source/documentation/index.rst +++ b/doc/source/documentation/index.rst @@ -4,7 +4,7 @@ Gerber Tools Reference .. toctree:: :maxdepth: 2 - Gerber (RS-274X) Files + Gerber (RS-274X) Files Excellon Files Rendering diff --git a/doc/source/documentation/rs274x.rst b/doc/source/documentation/rs274x.rst new file mode 100644 index 0000000..bc99519 --- /dev/null +++ b/doc/source/documentation/rs274x.rst @@ -0,0 +1,37 @@ +:mod:`rs274x` --- RS-274X file handling +============================================== + +.. module:: rs274x + :synopsis: Functions and classes for handling RS-274X files +.. sectionauthor:: Hamilton Kibbe + + +The RS-274X (Gerber) format is the most common format for exporting PCB +artwork. The Specification is published by Ucamco and is available +`here `_. +The :mod:`rs274x` submodule implements calsses to read and write +RS-274X files without having to know the precise details of the format. + +The :mod:`rs274x` submodule's :func:`read` function serves as a +simple interface for parsing gerber files. The :class:`GerberFile` class +stores all the information contained in a gerber file allowing the file to be +analyzed, modified, and updated. The :class:`GerberParser` class is used in +the background for parsing RS-274X files. + +.. _gerber-contents: + +Functions +--------- +The :mod:`rs274x` module defines the following functions: + +.. autofunction:: gerber.rs274x.read + +Classes +------- +The :mod:`rs274x` module defines the following classes: + +.. autoclass:: gerber.rs274x.GerberFile + :members: + +.. autoclass:: gerber.rs274x.GerberParser + :members: \ No newline at end of file diff --git a/examples/composite_bottom.png b/examples/composite_bottom.png index 76451a5..4d13bfd 100644 Binary files a/examples/composite_bottom.png and b/examples/composite_bottom.png differ diff --git a/examples/composite_bottom.svg b/examples/composite_bottom.svg new file mode 100644 index 0000000..c2e176d --- /dev/null +++ b/examples/composite_bottom.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/examples/composite_top.png b/examples/composite_top.png index a68bac8..d1dfe15 100644 Binary files a/examples/composite_top.png and b/examples/composite_top.png differ diff --git a/examples/composite_top.svg b/examples/composite_top.svg new file mode 100644 index 0000000..21b01fa --- /dev/null +++ b/examples/composite_top.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/gerber.md b/gerber.md deleted file mode 100644 index 865d177..0000000 --- a/gerber.md +++ /dev/null @@ -1,73 +0,0 @@ - -# Gerber (RS-274X or Extended Gerber) is a bilevel, resolution independent image format. - -# // graphic objects -# // draw: line segment, thickness, round or square line endings. (solid circle and rectangule apertures only) -# // arc: circular arc, thickness, round endings. (solid circle standard aperture only) -# // flash: replication of a given apertura (shape) -# // region: are defined by a countour (linear/arc segments.) -# -# // draw/arc: can have zero length (just flash the aperture) -# // flash: any aperture can be flashed -# -# // operation codes operates on coordinate data blocks. each operation code is for one coordinate data block pair and vice-versa. -# // D01: stroke an aperture from current point to coordinate pair. region mode off. lights-on move. -# // D02: move current point to this coordinate pair -# // D03: flash current aperture at this coordinate pair. -# -# // graphics state -# // all state controlled by codes and parameters, except current point -# // -# // state fixed? initial value -# // coordinate format fixed undefined -# // unit fixed undefined -# // image polarity fixed positive -# // steps/repeat variable 1,1,-,- -# // level polarity variable dark -# // region mode variable off -# // current aperture variable undefined -# // quadrant mode variable undefined -# // interpolation mode variable undefined -# // current point variable (0,0) -# -# // attributes: metadata, both standard and custom. No change on image. -# -# // G01: linear -# // G04: comment -# // M02: end of file -# // D: select aperture -# // G75: multi quadrant mode (circles) -# // G36: region begin -# // G37: region end -# -# // [G01] [Xnnfffff] [Ynnffff] D01* -# -# // ASCII 32-126, CR LF. -# // * end-of-block -# // % parameer delimiter -# // , field separator -# // only in comments -# // case sensitive -# -# // int: +/- 32 bit signed -# // decimal: +/- digits -# // names: [a-zA-Z_$]{[a-zA-Z_$0-9]+} (255) -# // strings: [a-zA-Z0-9_+-/!?<>”’(){}.\|&@# ]+ (65535) -# -# // data block: end in * -# // statement: one or more data block, if contain parameters starts and end in % (parameter statement) -# // statement: [%]{}[%] -# // statements: function code, coordinate data, parameters -# -# // function code: operation codes (D01..) or code that set state. -# // function codes applies before operation codes act on coordinates -# -# // coordinate data: : [X][Y][I][J](D01|D02|D03) -# // offsets are not modal -# -# // parameter: %Parameter code[optional modifiers]*% -# // code: 2 characters -# -# // parameters can have line separators: %{{}}% -# -# // function code: (GDM){1}[number], parameters: [AZ]{2} \ No newline at end of file diff --git a/gerber/__main__.py b/gerber/__main__.py index 1af4c0f..ab0f377 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -30,7 +30,6 @@ if __name__ == '__main__': print "parsing %s" % filename gerberfile = read(filename) gerberfile.render(ctx) - ctx.set_color(tuple([color * 0.4 for color in ctx.color])) print('Saving image to test.svg') ctx.dump('test.svg') diff --git a/gerber/common.py b/gerber/common.py index 0092ec8..6e8c862 100644 --- a/gerber/common.py +++ b/gerber/common.py @@ -30,12 +30,12 @@ def read(filename): CncFile object representing the file, either GerberFile or ExcellonFile. Returns None if file is not an Excellon or Gerber file. """ - import gerber + import rs274x import excellon from utils import detect_file_format fmt = detect_file_format(filename) if fmt == 'rs274x': - return gerber.read(filename) + return rs274x.read(filename) elif fmt == 'excellon': return excellon.read(filename) else: diff --git a/gerber/excellon.py b/gerber/excellon.py index f5d6c29..13aacc6 100755 --- a/gerber/excellon.py +++ b/gerber/excellon.py @@ -39,6 +39,7 @@ def read(filename): ------- file : :class:`gerber.excellon.ExcellonFile` An ExcellonFile created from the specified file. + """ detected_settings = detect_excellon_format(filename) settings = FileSettings(**detected_settings) @@ -317,7 +318,7 @@ def detect_excellon_format(filename): hole_area = 0.0 for hit in p.hits: tool = hit[0] - hole_area += math.pow(math.pi * tool.diameter, 2) + hole_area += math.pow(math.pi * tool.diameter / 2., 2) results[key] = (size, p.hole_count, hole_area) except: pass diff --git a/gerber/gerber.py b/gerber/gerber.py deleted file mode 100644 index 335b443..0000000 --- a/gerber/gerber.py +++ /dev/null @@ -1,332 +0,0 @@ -#! /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. -""" This module provides an RS-274-X class and parser. -""" - - -import re -import json -from .gerber_statements import * -from .cam import CamFile, FileSettings - - - - -def read(filename): - """ Read data from filename and return a GerberFile - - Parameters - ---------- - filename : string - Filename of file to parse - - Returns - ------- - file : :class:`gerber.gerber.GerberFile` - A GerberFile created from the specified file. - """ - return GerberParser().parse(filename) - - -class GerberFile(CamFile): - """ A class representing a single gerber file - - The GerberFile class represents a single gerber file. - - Parameters - ---------- - statements : list - list of gerber file statements - - settings : dict - Dictionary of gerber file settings - - filename : string - Filename of the source gerber file - - Attributes - ---------- - comments: list of strings - List of comments contained in the gerber file. - - size : tuple, (, ) - Size in [self.units] of the layer described by the gerber file. - - bounds: tuple, ((, ), (, )) - boundaries of the layer described by the gerber file. - `bounds` is stored as ((min x, max x), (min y, max y)) - - """ - def __init__(self, statements, settings, filename=None): - super(GerberFile, self).__init__(statements, settings, filename) - - @property - def comments(self): - return [comment.comment for comment in self.statements - if isinstance(comment, CommentStmt)] - - @property - def size(self): - xbounds, ybounds = self.bounds - return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) - - @property - def bounds(self): - xbounds = [0.0, 0.0] - ybounds = [0.0, 0.0] - for stmt in [stmt for stmt in self.statements - if isinstance(stmt, CoordStmt)]: - if stmt.x is not None and stmt.x < xbounds[0]: - xbounds[0] = stmt.x - if stmt.x is not None and stmt.x > xbounds[1]: - xbounds[1] = stmt.x - if stmt.i is not None and stmt.i < xbounds[0]: - xbounds[0] = stmt.i - if stmt.i is not None and stmt.i > xbounds[1]: - xbounds[1] = stmt.i - if stmt.y is not None and stmt.y < ybounds[0]: - ybounds[0] = stmt.y - if stmt.y is not None and stmt.y > ybounds[1]: - ybounds[1] = stmt.y - if stmt.j is not None and stmt.j < ybounds[0]: - ybounds[0] = stmt.j - if stmt.j is not None and stmt.j > ybounds[1]: - ybounds[1] = stmt.j - return (xbounds, ybounds) - - def write(self, filename): - """ Write data out to a gerber file - """ - with open(filename, 'w') as f: - for statement in self.statements: - f.write(statement.to_gerber()) - - def render(self, ctx, filename=None): - """ Generate image of layer. - - Parameters - ---------- - ctx : :class:`GerberContext` - GerberContext subclass used for rendering the image - - filename : string - If provided, the rendered image will be saved to `filename` - """ - ctx.set_bounds(self.bounds) - for statement in self.statements: - ctx.evaluate(statement) - if filename is not None: - ctx.dump(filename) - - -class GerberParser(object): - """ GerberParser - """ - NUMBER = r"[\+-]?\d+" - DECIMAL = r"[\+-]?\d+([.]?\d+)?" - STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" - NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" - FUNCTION = r"G\d{2}" - - COORD_OP = r"D[0]?[123]" - - FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" - MO = r"(?PMO)(?P(MM|IN))" - IP = r"(?PIP)(?P(POS|NEG))" - LP = r"(?PLP)(?P(D|C))" - AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" - AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" - AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" - AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" - AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) - AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) - - # begin deprecated - OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) - IN = r"(?PIN)(?P.*)" - LN = r"(?PLN)(?P.*)" - # end deprecated - - PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) - PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] - - COORD_STMT = re.compile(( - r"(?P{function})?" - r"(X(?P{number}))?(Y(?P{number}))?" - r"(I(?P{number}))?(J(?P{number}))?" - r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) - - APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") - - COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") - - EOF_STMT = re.compile(r"(?PM02)\*") - - REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') - QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') - - def __init__(self): - self.settings = FileSettings() - self.statements = [] - - def parse(self, filename): - fp = open(filename, "r") - data = fp.readlines() - - for stmt in self._parse(data): - self.statements.append(stmt) - - return GerberFile(self.statements, self.settings, filename) - - def dump_json(self): - stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} - return json.dumps(stmts) - - def dump_str(self): - s = "" - for stmt in self.statements: - s += str(stmt) + "\n" - return s - - def _parse(self, data): - oldline = '' - - for i, line in enumerate(data): - line = oldline + line.strip() - - # skip empty lines - if not len(line): - continue - - # deal with multi-line parameters - if line.startswith("%") and not line.endswith("%"): - oldline = line - continue - - did_something = True # make sure we do at least one loop - while did_something and len(line) > 0: - did_something = False - - # Region Mode - (mode, r) = self._match_one(self.REGION_MODE_STMT, line) - if mode: - yield RegionModeStmt.from_gerber(line) - line = r - did_something = True - continue - - # Quadrant Mode - (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) - if mode: - yield QuadrantModeStmt.from_gerber(line) - line = r - did_something = True - continue - - # coord - (coord, r) = self._match_one(self.COORD_STMT, line) - if coord: - yield CoordStmt.from_dict(coord, self.settings) - line = r - did_something = True - continue - - # aperture selection - (aperture, r) = self._match_one(self.APERTURE_STMT, line) - if aperture: - yield ApertureStmt(**aperture) - - did_something = True - line = r - continue - - # comment - (comment, r) = self._match_one(self.COMMENT_STMT, line) - if comment: - yield CommentStmt(comment["comment"]) - did_something = True - line = r - continue - - # parameter - (param, r) = self._match_one_from_many(self.PARAM_STMT, line) - if param: - if param["param"] == "FS": - stmt = FSParamStmt.from_dict(param) - self.settings.zero_suppression = stmt.zero_suppression - self.settings.format = stmt.format - self.settings.notation = stmt.notation - yield stmt - elif param["param"] == "MO": - stmt = MOParamStmt.from_dict(param) - self.settings.units = stmt.mode - yield stmt - elif param["param"] == "IP": - yield IPParamStmt.from_dict(param) - elif param["param"] == "LP": - yield LPParamStmt.from_dict(param) - elif param["param"] == "AD": - yield ADParamStmt.from_dict(param) - elif param["param"] == "AM": - yield AMParamStmt.from_dict(param) - elif param["param"] == "OF": - yield OFParamStmt.from_dict(param) - elif param["param"] == "IN": - yield INParamStmt.from_dict(param) - elif param["param"] == "LN": - yield LNParamStmt.from_dict(param) - else: - yield UnknownStmt(line) - did_something = True - line = r - continue - - # eof - (eof, r) = self._match_one(self.EOF_STMT, line) - if eof: - yield EofStmt() - did_something = True - line = r - continue - - if False: - print self.COORD_STMT.pattern - print self.APERTURE_STMT.pattern - print self.COMMENT_STMT.pattern - print self.EOF_STMT.pattern - for i in self.PARAM_STMT: - print i.pattern - - if line.find('*') > 0: - yield UnknownStmt(line) - oldline = line - - def _match_one(self, expr, data): - match = expr.match(data) - if match is None: - return ({}, None) - else: - return (match.groupdict(), data[match.end(0):]) - - def _match_one_from_many(self, exprs, data): - for expr in exprs: - match = expr.match(data) - if match: - return (match.groupdict(), data[match.end(0):]) - - return ({}, None) diff --git a/gerber/render/render.py b/gerber/render/render.py index 48a53f8..f7e4485 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -83,6 +83,9 @@ class GerberContext(object): background_color : tuple (, , ) Color of the background. Used when exposing areas in 'clear' level polarity mode. Format is the same as for `color`. + + alpha : float + Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ def __init__(self): self.settings = {} @@ -100,7 +103,8 @@ class GerberContext(object): self.color = (0.7215, 0.451, 0.200) self.drill_color = (0.25, 0.25, 0.25) self.background_color = (0.0, 0.0, 0.0) - + self.alpha = 1.0 + def set_format(self, settings): """ Set source file format. @@ -260,6 +264,19 @@ class GerberContext(object): """ self.background_color = color + def set_alpha(self, alpha): + """ Set layer rendering opacity + + .. note:: + Not all backends/rendering devices support this parameter. + + Parameters + ---------- + alpha : float + Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque) + """ + self.alpha = alpha + def resolve(self, x, y): """ Resolve missing x or y coordinates in a coordinate command. diff --git a/gerber/render/svgwrite_backend.py b/gerber/render/svgwrite_backend.py index 886b4f8..78961da 100644 --- a/gerber/render/svgwrite_backend.py +++ b/gerber/render/svgwrite_backend.py @@ -117,6 +117,7 @@ class GerberSvgContext(GerberContext): self.apertures = {} self.dwg = svgwrite.Drawing() + self.dwg.transform = 'scale 1 -1' self.background = False self.region_path = None @@ -124,11 +125,17 @@ class GerberSvgContext(GerberContext): xbounds, ybounds = bounds size = (SCALE * (xbounds[1] - xbounds[0]), SCALE * (ybounds[1] - ybounds[0])) if not self.background: + self.dwg = svgwrite.Drawing(viewBox='%f, %f, %f, %f' % (SCALE*xbounds[0], -SCALE*ybounds[1],size[0], size[1])) self.dwg.add(self.dwg.rect(insert=(SCALE * xbounds[0], -SCALE * ybounds[1]), size=size, fill=convert_color(self.background_color))) self.background = True + def set_alpha(self, alpha): + super(GerberSvgContext, self).set_alpha(alpha) + import warnings + warnings.warn('SVG output does not support transparency') + def define_aperture(self, d, shape, modifiers): aperture = None if shape == 'C': diff --git a/gerber/rs274x.py b/gerber/rs274x.py new file mode 100644 index 0000000..4076f77 --- /dev/null +++ b/gerber/rs274x.py @@ -0,0 +1,327 @@ +#! /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. +""" This module provides an RS-274-X class and parser. +""" + + +import re +import json +from .gerber_statements import * +from .cam import CamFile, FileSettings + + + + +def read(filename): + """ Read data from filename and return a GerberFile + + Parameters + ---------- + filename : string + Filename of file to parse + + Returns + ------- + file : :class:`gerber.rs274x.GerberFile` + A GerberFile created from the specified file. + """ + return GerberParser().parse(filename) + + +class GerberFile(CamFile): + """ A class representing a single gerber file + + The GerberFile class represents a single gerber file. + + Parameters + ---------- + statements : list + list of gerber file statements + + settings : dict + Dictionary of gerber file settings + + filename : string + Filename of the source gerber file + + Attributes + ---------- + comments: list of strings + List of comments contained in the gerber file. + + size : tuple, (, ) + Size in [self.units] of the layer described by the gerber file. + + bounds: tuple, ((, ), (, )) + boundaries of the layer described by the gerber file. + `bounds` is stored as ((min x, max x), (min y, max y)) + + """ + def __init__(self, statements, settings, filename=None): + super(GerberFile, self).__init__(statements, settings, filename) + + @property + def comments(self): + return [comment.comment for comment in self.statements + if isinstance(comment, CommentStmt)] + + @property + def size(self): + xbounds, ybounds = self.bounds + return (xbounds[1] - xbounds[0], ybounds[1] - ybounds[0]) + + @property + def bounds(self): + xbounds = [0.0, 0.0] + ybounds = [0.0, 0.0] + for stmt in [stmt for stmt in self.statements + if isinstance(stmt, CoordStmt)]: + if stmt.x is not None: + if stmt.x < xbounds[0]: + xbounds[0] = stmt.x + elif stmt.x > xbounds[1]: + xbounds[1] = stmt.x + if stmt.y is not None: + if stmt.y < ybounds[0]: + ybounds[0] = stmt.y + elif stmt.y > ybounds[1]: + ybounds[1] = stmt.y + return (xbounds, ybounds) + + + def write(self, filename): + """ Write data out to a gerber file + """ + with open(filename, 'w') as f: + for statement in self.statements: + f.write(statement.to_gerber()) + + def render(self, ctx, filename=None): + """ Generate image of layer. + + Parameters + ---------- + ctx : :class:`GerberContext` + GerberContext subclass used for rendering the image + + filename : string + If provided, the rendered image will be saved to `filename` + """ + ctx.set_bounds(self.bounds) + for statement in self.statements: + ctx.evaluate(statement) + if filename is not None: + ctx.dump(filename) + + +class GerberParser(object): + """ GerberParser + """ + NUMBER = r"[\+-]?\d+" + DECIMAL = r"[\+-]?\d+([.]?\d+)?" + STRING = r"[a-zA-Z0-9_+\-/!?<>”’(){}.\|&@# :]+" + NAME = "[a-zA-Z_$][a-zA-Z_$0-9]+" + FUNCTION = r"G\d{2}" + + COORD_OP = r"D[0]?[123]" + + FS = r"(?PFS)(?P(L|T))?(?P(A|I))X(?P[0-7][0-7])Y(?P[0-7][0-7])" + MO = r"(?PMO)(?P(MM|IN))" + IP = r"(?PIP)(?P(POS|NEG))" + LP = r"(?PLP)(?P(D|C))" + AD_CIRCLE = r"(?PAD)D(?P\d+)(?PC)[,](?P[^,]*)" + AD_RECT = r"(?PAD)D(?P\d+)(?PR)[,](?P[^,]*)" + AD_OBROUND = r"(?PAD)D(?P\d+)(?PO)[,](?P[^,]*)" + AD_POLY = r"(?PAD)D(?P\d+)(?PP)[,](?P[^,]*)" + AD_MACRO = r"(?PAD)D(?P\d+)+(?P{name})[,](?P[^,]*)".format(name=NAME) + AM = r"(?PAM)(?P{name})\*(?P.*)".format(name=NAME) + + # begin deprecated + OF = r"(?POF)(A(?P{decimal}))?(B(?P{decimal}))?".format(decimal=DECIMAL) + IN = r"(?PIN)(?P.*)" + LN = r"(?PLN)(?P.*)" + # end deprecated + + PARAMS = (FS, MO, IP, LP, AD_CIRCLE, AD_RECT, AD_OBROUND, AD_MACRO, AD_POLY, AM, OF, IN, LN) + PARAM_STMT = [re.compile(r"%{0}\*%".format(p)) for p in PARAMS] + + COORD_STMT = re.compile(( + r"(?P{function})?" + r"(X(?P{number}))?(Y(?P{number}))?" + r"(I(?P{number}))?(J(?P{number}))?" + r"(?P{op})?\*".format(number=NUMBER, function=FUNCTION, op=COORD_OP))) + + APERTURE_STMT = re.compile(r"(G54)?D(?P\d+)\*") + + COMMENT_STMT = re.compile(r"G04(?P[^*]*)(\*)?") + + EOF_STMT = re.compile(r"(?PM02)\*") + + REGION_MODE_STMT = re.compile(r'(?PG3[67])\*') + QUAD_MODE_STMT = re.compile(r'(?PG7[45])\*') + + def __init__(self): + self.settings = FileSettings() + self.statements = [] + + def parse(self, filename): + fp = open(filename, "r") + data = fp.readlines() + + for stmt in self._parse(data): + self.statements.append(stmt) + + return GerberFile(self.statements, self.settings, filename) + + def dump_json(self): + stmts = {"statements": [stmt.__dict__ for stmt in self.statements]} + return json.dumps(stmts) + + def dump_str(self): + s = "" + for stmt in self.statements: + s += str(stmt) + "\n" + return s + + def _parse(self, data): + oldline = '' + + for i, line in enumerate(data): + line = oldline + line.strip() + + # skip empty lines + if not len(line): + continue + + # deal with multi-line parameters + if line.startswith("%") and not line.endswith("%"): + oldline = line + continue + + did_something = True # make sure we do at least one loop + while did_something and len(line) > 0: + did_something = False + + # Region Mode + (mode, r) = self._match_one(self.REGION_MODE_STMT, line) + if mode: + yield RegionModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # Quadrant Mode + (mode, r) = self._match_one(self.QUAD_MODE_STMT, line) + if mode: + yield QuadrantModeStmt.from_gerber(line) + line = r + did_something = True + continue + + # coord + (coord, r) = self._match_one(self.COORD_STMT, line) + if coord: + yield CoordStmt.from_dict(coord, self.settings) + line = r + did_something = True + continue + + # aperture selection + (aperture, r) = self._match_one(self.APERTURE_STMT, line) + if aperture: + yield ApertureStmt(**aperture) + + did_something = True + line = r + continue + + # comment + (comment, r) = self._match_one(self.COMMENT_STMT, line) + if comment: + yield CommentStmt(comment["comment"]) + did_something = True + line = r + continue + + # parameter + (param, r) = self._match_one_from_many(self.PARAM_STMT, line) + if param: + if param["param"] == "FS": + stmt = FSParamStmt.from_dict(param) + self.settings.zero_suppression = stmt.zero_suppression + self.settings.format = stmt.format + self.settings.notation = stmt.notation + yield stmt + elif param["param"] == "MO": + stmt = MOParamStmt.from_dict(param) + self.settings.units = stmt.mode + yield stmt + elif param["param"] == "IP": + yield IPParamStmt.from_dict(param) + elif param["param"] == "LP": + yield LPParamStmt.from_dict(param) + elif param["param"] == "AD": + yield ADParamStmt.from_dict(param) + elif param["param"] == "AM": + yield AMParamStmt.from_dict(param) + elif param["param"] == "OF": + yield OFParamStmt.from_dict(param) + elif param["param"] == "IN": + yield INParamStmt.from_dict(param) + elif param["param"] == "LN": + yield LNParamStmt.from_dict(param) + else: + yield UnknownStmt(line) + did_something = True + line = r + continue + + # eof + (eof, r) = self._match_one(self.EOF_STMT, line) + if eof: + yield EofStmt() + did_something = True + line = r + continue + + if False: + print self.COORD_STMT.pattern + print self.APERTURE_STMT.pattern + print self.COMMENT_STMT.pattern + print self.EOF_STMT.pattern + for i in self.PARAM_STMT: + print i.pattern + + if line.find('*') > 0: + yield UnknownStmt(line) + oldline = line + + def _match_one(self, expr, data): + match = expr.match(data) + if match is None: + return ({}, None) + else: + return (match.groupdict(), data[match.end(0):]) + + def _match_one_from_many(self, exprs, data): + for expr in exprs: + match = expr.match(data) + if match: + return (match.groupdict(), data[match.end(0):]) + + return ({}, None) -- cgit