Cleanup, rendering fixes.

fixed rendering of tented vias
fixed rendering of semi-transparent layers
fixed file type detection issues
added some examples
refactor
Hamilton Kibbe 2016-01-28 12:19:03 -05:00
rodzic b9f1b106c3
commit 5df38c014f
26 zmienionych plików z 401 dodań i 231 usunięć

4
.gitignore vendored
Wyświetl plik

@ -34,8 +34,6 @@ nosetests.xml
.mr.developer.cfg
.project
.pydevproject
.idea/workspace.xml
.idea/misc.xml
.idea
# Komodo Files
@ -43,4 +41,4 @@ nosetests.xml
# OS Files
.DS_Store
Thumbs.db
Thumbs.db

Wyświetl plik

@ -20,6 +20,10 @@ test-coverage:
rm -rf coverage .coverage
$(NOSETESTS) -s -v --with-coverage --cover-package=gerber
.PHONY: install
install:
PYTHONPATH=. $(PYTHON) setup.py install
.PHONY: doc-html
doc-html:
(cd $(DOC_ROOT); make html)

Wyświetl plik

@ -6,7 +6,7 @@ pcb-tools
Tools to handle Gerber and Excellon files in Python.
Useage Example:
Usage Example:
---------------
import gerber
from gerber.render import GerberCairoContext
@ -27,6 +27,7 @@ Rendering Examples:
-------------------
###Top Composite rendering
![Composite Top Image](examples/cairo_example.png)
![Composite Bottom Image](examples/cairo_bottom.png)
Source code for this example can be found [here](examples/cairo_example.py).

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 42 KiB

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 102 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 98 KiB

Wyświetl plik

@ -24,46 +24,54 @@ a .png file.
"""
import os
from gerber import read
from gerber.render import GerberCairoContext, theme
from gerber import load_layer
from gerber.render import GerberCairoContext, RenderSettings, theme
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
# Open the gerber files
copper = read(os.path.join(GERBER_FOLDER, 'copper.GTL'))
mask = read(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))
silk = read(os.path.join(GERBER_FOLDER, 'silkscreen.GTO'))
drill = read(os.path.join(GERBER_FOLDER, 'ncdrill.DRD'))
copper = load_layer(os.path.join(GERBER_FOLDER, 'copper.GTL'))
mask = load_layer(os.path.join(GERBER_FOLDER, 'soldermask.GTS'))
silk = load_layer(os.path.join(GERBER_FOLDER, 'silkscreen.GTO'))
drill = load_layer(os.path.join(GERBER_FOLDER, 'ncdrill.DRD'))
# Create a new drawing context
ctx = GerberCairoContext()
# Set opacity and color for copper layer
ctx.alpha = 1.0
ctx.color = theme.COLORS['hasl copper']
# Draw the copper layer
copper.render(ctx)
# Set opacity and color for soldermask layer
ctx.alpha = 0.75
ctx.color = theme.COLORS['green soldermask']
# Draw the copper layer. render_layer() uses the default color scheme for the
# layer, based on the layer type. Copper layers are rendered as
ctx.render_layer(copper)
# Draw the soldermask layer
mask.render(ctx, invert=True)
ctx.render_layer(mask)
# Set opacity and color for silkscreen layer
ctx.alpha = 1.0
ctx.color = theme.COLORS['white']
# Draw the silkscreen layer
silk.render(ctx)
# The default style can be overridden by passing a RenderSettings instance to
# render_layer().
# First, create a settings object:
our_settings = RenderSettings(color=theme.COLORS['white'], alpha=0.85)
# Set opacity for drill layer
ctx.alpha = 1.0
ctx.color = theme.COLORS['black']
drill.render(ctx)
# Draw the silkscreen layer, and specify the rendering settings to use
ctx.render_layer(silk, settings=our_settings)
# Draw the drill layer
ctx.render_layer(drill)
# Write output to png file
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_example.png'))
# Load the bottom layers
copper = load_layer(os.path.join(GERBER_FOLDER, 'bottom_copper.GBL'))
mask = load_layer(os.path.join(GERBER_FOLDER, 'bottom_mask.GBS'))
# Clear the drawing
ctx.clear()
# Render bottom layers
ctx.render_layer(copper)
ctx.render_layer(mask)
ctx.render_layer(drill)
# Write png file
ctx.dump(os.path.join(os.path.dirname(__file__), 'cairo_bottom.png'))

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 40 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 36 KiB

Wyświetl plik

@ -1,7 +1,7 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be>
# Copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -27,14 +27,25 @@ from gerber.render import GerberCairoContext, theme
GERBER_FOLDER = os.path.abspath(os.path.join(os.path.dirname(__file__), 'gerbers'))
# Create a new drawing context
ctx = GerberCairoContext()
# Create a new PCB
# Create a new PCB instance
pcb = PCB.from_directory(GERBER_FOLDER)
# Render PCB
ctx.render_layers(pcb.top_layers, os.path.join(os.path.dirname(__file__), 'pcb_top.png',), theme.THEMES['OSH Park'])
ctx.render_layers(pcb.bottom_layers, os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'), theme.THEMES['OSH Park'])
# Render PCB top view
ctx.render_layers(pcb.top_layers,
os.path.join(os.path.dirname(__file__), 'pcb_top.png',),
theme.THEMES['OSH Park'])
# Render PCB bottom view
ctx.render_layers(pcb.bottom_layers,
os.path.join(os.path.dirname(__file__), 'pcb_bottom.png'),
theme.THEMES['OSH Park'])
# Render copper layers only
ctx.render_layers(pcb.copper_layers + pcb.drill_layers,
os.path.join(os.path.dirname(__file__),
'pcb_transparent_copper.png'),
theme.THEMES['Transparent Copper'])

Plik binarny nie jest wyświetlany.

Przed

Szerokość:  |  Wysokość:  |  Rozmiar: 96 KiB

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 90 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 83 KiB

Wyświetl plik

@ -24,4 +24,5 @@ files in python.
"""
from .common import read, loads
from .layers import load_layer, load_layer_data
from .pcb import PCB

Wyświetl plik

@ -251,7 +251,7 @@ class CamFile(object):
def to_metric(self):
pass
def render(self, ctx, invert=False, filename=None):
def render(self, ctx=None, invert=False, filename=None):
""" Generate image of layer.
Parameters
@ -262,13 +262,16 @@ class CamFile(object):
filename : string <optional>
If provided, save the rendered image to `filename`
"""
if ctx is None:
from .render import GerberCairoContext
ctx = GerberCairoContext()
ctx.set_bounds(self.bounds)
ctx._paint_background()
ctx.invert = invert
ctx._new_render_layer()
for p in self.primitives:
ctx.render(p)
ctx._flatten()
ctx._paint()
if filename is not None:
ctx.dump(filename)

Wyświetl plik

@ -33,42 +33,41 @@ def read(filename):
Returns
-------
file : CncFile subclass
CncFile object representing the file, either GerberFile or
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
CncFile object representing the file, either GerberFile, ExcellonFile,
or IPCNetlist. Returns None if file is not of the proper type.
"""
with open(filename, 'rU') as f:
data = f.read()
fmt = detect_file_format(data)
if fmt == 'rs274x':
return rs274x.read(filename)
elif fmt == 'excellon':
return excellon.read(filename)
elif fmt == 'ipc_d_356':
return ipc356.read(filename)
else:
raise ParseError('Unable to detect file format')
return loads(data, filename)
def loads(data):
def loads(data, filename=None):
""" Read gerber or excellon file contents from a string and return a
representative object.
Parameters
----------
data : string
gerber or excellon file contents as a string.
Source file contents as a string.
filename : string, optional
String containing the filename of the data source.
Returns
-------
file : CncFile subclass
CncFile object representing the file, either GerberFile or
ExcellonFile. Returns None if file is not an Excellon or Gerber file.
CncFile object representing the data, either GerberFile, ExcellonFile,
or IPCNetlist. Returns None if data is not of the proper type.
"""
fmt = detect_file_format(data)
if fmt == 'rs274x':
return rs274x.loads(data)
return rs274x.loads(data, filename)
elif fmt == 'excellon':
return excellon.loads(data)
return excellon.loads(data, filename)
elif fmt == 'ipc_d_356':
return ipc356.loads(data, filename)
else:
raise TypeError('Unable to detect file format')
raise ParseError('Unable to detect file format')

Wyświetl plik

@ -28,7 +28,7 @@ import operator
try:
from cStringIO import StringIO
except(ImportError):
except ImportError:
from io import StringIO
from .excellon_statements import *
@ -57,13 +57,16 @@ def read(filename):
return ExcellonParser(settings).parse(filename)
def loads(data):
def loads(data, filename=None):
""" Read data from string and return an ExcellonFile
Parameters
----------
data : string
string containing Excellon file contents
filename : string, optional
string containing the filename of the data source
Returns
-------
file : :class:`gerber.excellon.ExcellonFile`
@ -72,7 +75,7 @@ def loads(data):
"""
# File object should use settings from source file by default.
settings = FileSettings(**detect_excellon_format(data))
return ExcellonParser(settings).parse_raw(data)
return ExcellonParser(settings).parse_raw(data, filename)
class DrillHit(object):

Wyświetl plik

@ -35,7 +35,7 @@ _SM_FIELD = {
def read(filename):
""" Read data from filename and return an IPC_D_356
""" Read data from filename and return an IPCNetlist
Parameters
----------
filename : string
@ -43,19 +43,38 @@ def read(filename):
Returns
-------
file : :class:`gerber.ipc356.IPC_D_356`
An IPC_D_356 object created from the specified file.
file : :class:`gerber.ipc356.IPCNetlist`
An IPCNetlist object created from the specified file.
"""
# File object should use settings from source file by default.
return IPC_D_356.from_file(filename)
return IPCNetlist.from_file(filename)
class IPC_D_356(CamFile):
def loads(data, filename=None):
""" Generate an IPCNetlist object from IPC-D-356 data in memory
Parameters
----------
data : string
string containing netlist file contents
filename : string, optional
string containing the filename of the data source
Returns
-------
file : :class:`gerber.ipc356.IPCNetlist`
An IPCNetlist created from the specified file.
"""
return IPCNetlistParser().parse_raw(data, filename)
class IPCNetlist(CamFile):
@classmethod
def from_file(cls, filename):
parser = IPC_D_356_Parser()
parser = IPCNetlistParser()
return parser.parse(filename)
def __init__(self, statements, settings, primitives=None, filename=None):
@ -130,7 +149,7 @@ class IPC_D_356(CamFile):
ctx.dump(filename)
class IPC_D_356_Parser(object):
class IPCNetlistParser(object):
# TODO: Allow multi-line statements (e.g. Altium board edge)
def __init__(self):
@ -145,9 +164,13 @@ class IPC_D_356_Parser(object):
def parse(self, filename):
with open(filename, 'rU') as f:
oldline = ''
for line in f:
# Check for existing multiline data...
data = f.read()
return self.parse_raw(data, filename)
def parse_raw(self, data, filename=None):
oldline = ''
for line in data.splitlines():
# Check for existing multiline data...
if oldline != '':
if len(line) and line[0] == '0':
oldline = oldline.rstrip('\r\n') + line[3:].rstrip()
@ -158,7 +181,7 @@ class IPC_D_356_Parser(object):
oldline = line
self._parse_line(oldline)
return IPC_D_356(self.statements, self.settings, filename=filename)
return IPCNetlist(self.statements, self.settings, filename=filename)
def _parse_line(self, line):
if not len(line):

Wyświetl plik

@ -19,8 +19,9 @@ import os
import re
from collections import namedtuple
from . import common
from .excellon import ExcellonFile
from .ipc356 import IPC_D_356
from .ipc356 import IPCNetlist
Hint = namedtuple('Hint', 'layer ext name')
@ -73,9 +74,21 @@ hints = [
ext=['ipc'],
name=[],
),
Hint(layer='drawing',
ext=['fab'],
name=['assembly drawing', 'assembly', 'fabrication', 'fab drawing']
),
]
def load_layer(filename):
return PCBLayer.from_cam(common.read(filename))
def load_layer_data(data, filename=None):
return PCBLayer.from_cam(common.loads(data, filename))
def guess_layer_class(filename):
try:
directory, name = os.path.split(filename)
@ -89,24 +102,30 @@ def guess_layer_class(filename):
return 'unknown'
def sort_layers(layers):
def sort_layers(layers, from_top=True):
layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
'internal', 'bottom', 'bottommask', 'bottomsilk',
'bottompaste', 'drill', ]
'bottompaste']
append_after = ['drill', 'drawing']
output = []
drill_layers = [layer for layer in layers if layer.layer_class == 'drill']
internal_layers = list(sorted([layer for layer in layers
if layer.layer_class == 'internal']))
for layer_class in layer_order:
if layer_class == 'internal':
output += internal_layers
elif layer_class == 'drill':
output += drill_layers
else:
for layer in layers:
if layer.layer_class == layer_class:
output.append(layer)
if not from_top:
output = list(reversed(output))
for layer_class in append_after:
for layer in layers:
if layer.layer_class == layer_class:
output.append(layer)
return output
@ -126,14 +145,14 @@ class PCBLayer(object):
"""
@classmethod
def from_gerber(cls, camfile):
def from_cam(cls, camfile):
filename = camfile.filename
layer_class = guess_layer_class(filename)
if isinstance(camfile, ExcellonFile) or (layer_class == 'drill'):
return DrillLayer.from_gerber(camfile)
return DrillLayer.from_cam(camfile)
elif layer_class == 'internal':
return InternalLayer.from_gerber(camfile)
if isinstance(camfile, IPC_D_356):
return InternalLayer.from_cam(camfile)
if isinstance(camfile, IPCNetlist):
layer_class = 'ipc_netlist'
return cls(filename, layer_class, camfile)
@ -155,9 +174,10 @@ class PCBLayer(object):
def __repr__(self):
return '<PCBLayer: {}>'.format(self.layer_class)
class DrillLayer(PCBLayer):
@classmethod
def from_gerber(cls, camfile):
def from_cam(cls, camfile):
return cls(camfile.filename, camfile)
def __init__(self, filename=None, cam_source=None, layers=None, **kwargs):
@ -168,11 +188,11 @@ class DrillLayer(PCBLayer):
class InternalLayer(PCBLayer):
@classmethod
def from_gerber(cls, camfile):
def from_cam(cls, camfile):
filename = camfile.filename
try:
order = int(re.search(r'\d+', filename).group())
except:
except AttributeError:
order = 0
return cls(filename, camfile, order)
@ -209,23 +229,3 @@ class InternalLayer(PCBLayer):
if not hasattr(other, 'order'):
raise TypeError()
return (self.order <= other.order)
class LayerSet(object):
def __init__(self, name, layers, **kwargs):
super(LayerSet, self).__init__(**kwargs)
self.name = name
self.layers = list(layers)
def __len__(self):
return len(self.layers)
def __getitem__(self, item):
return self.layers[item]
def to_render(self):
return self.layers
def apply_theme(self, theme):
pass

Wyświetl plik

@ -18,7 +18,7 @@
import os
from .exceptions import ParseError
from .layers import PCBLayer, LayerSet, sort_layers
from .layers import PCBLayer, sort_layers
from .common import read as gerber_read
from .utils import listdir
@ -29,22 +29,26 @@ class PCB(object):
def from_directory(cls, directory, board_name=None, verbose=False):
layers = []
names = set()
# Validate
directory = os.path.abspath(directory)
if not os.path.isdir(directory):
raise TypeError('{} is not a directory.'.format(directory))
# Load gerber files
for filename in listdir(directory, True, True):
try:
camfile = gerber_read(os.path.join(directory, filename))
layer = PCBLayer.from_gerber(camfile)
layer = PCBLayer.from_cam(camfile)
layers.append(layer)
names.add(os.path.splitext(filename)[0])
if verbose:
print('Added {} layer <{}>'.format(layer.layer_class, filename))
print('[PCB]: Added {} layer <{}>'.format(layer.layer_class,
filename))
except ParseError:
if verbose:
print('Skipping file {}'.format(filename))
print('[PCB]: Skipping file {}'.format(filename))
# Try to guess board name
if board_name is None:
if len(names) == 1:
@ -66,14 +70,16 @@ class PCB(object):
board_layers = [l for l in reversed(self.layers) if l.layer_class in
('topsilk', 'topmask', 'top')]
drill_layers = [l for l in self.drill_layers if 'top' in l.layers]
return board_layers + drill_layers
# Drill layer goes under soldermask for proper rendering of tented vias
return [board_layers[0]] + drill_layers + board_layers[1:]
@property
def bottom_layers(self):
board_layers = [l for l in self.layers if l.layer_class in
('bottomsilk', 'bottommask', 'bottom')]
drill_layers = [l for l in self.drill_layers if 'bottom' in l.layers]
return board_layers + drill_layers
# Drill layer goes under soldermask for proper rendering of tented vias
return [board_layers[0]] + drill_layers + board_layers[1:]
@property
def drill_layers(self):
@ -81,8 +87,9 @@ class PCB(object):
@property
def copper_layers(self):
return [layer for layer in self.layers if layer.layer_class in
('top', 'bottom', 'internal')]
return list(reversed([layer for layer in self.layers if
layer.layer_class in
('top', 'bottom', 'internal')]))
@property
def layer_count(self):

Wyświetl plik

@ -166,7 +166,6 @@ class Primitive(object):
in zip(self.position,
(x_offset, y_offset))])
def _changed(self):
""" Clear memoized properties.
@ -568,11 +567,11 @@ class Rectangle(Primitive):
@property
def axis_aligned_width(self):
return (self._cos_theta * self.width + self._sin_theta * self.height)
return (self._cos_theta * self.width) + (self._sin_theta * self.height)
@property
def axis_aligned_height(self):
return (self._cos_theta * self.height + self._sin_theta * self.width)
return (self._cos_theta * self.height) + (self._sin_theta * self.width)
class Diamond(Primitive):
@ -640,25 +639,24 @@ class Diamond(Primitive):
@property
def axis_aligned_width(self):
return (self._cos_theta * self.width + self._sin_theta * self.height)
return (self._cos_theta * self.width) + (self._sin_theta * self.height)
@property
def axis_aligned_height(self):
return (self._cos_theta * self.height + self._sin_theta * self.width)
return (self._cos_theta * self.height) + (self._sin_theta * self.width)
class ChamferRectangle(Primitive):
"""
"""
def __init__(self, position, width, height, chamfer, corners, **kwargs):
def __init__(self, position, width, height, chamfer, corners=None, **kwargs):
super(ChamferRectangle, self).__init__(**kwargs)
validate_coordinates(position)
self._position = position
self._width = width
self._height = height
self._chamfer = chamfer
self._corners = corners
self._corners = corners if corners is not None else [True] * 4
self._to_convert = ['position', 'width', 'height', 'chamfer']
@property
@ -718,7 +716,37 @@ class ChamferRectangle(Primitive):
@property
def vertices(self):
# TODO
if self._vertices is None:
vertices = []
delta_w = self.width / 2.
delta_h = self.height / 2.
# order is UR, UL, LL, LR
rect_corners = [
((self.position[0] + delta_w), (self.position[1] + delta_h)),
((self.position[0] - delta_w), (self.position[1] + delta_h)),
((self.position[0] - delta_w), (self.position[1] - delta_h)),
((self.position[0] + delta_w), (self.position[1] - delta_h))
]
for idx, corner, chamfered in enumerate((rect_corners, self.corners)):
x, y = corner
if chamfered:
if idx == 0:
vertices.append((x - self.chamfer, y))
vertices.append((x, y - self.chamfer))
elif idx == 1:
vertices.append((x + self.chamfer, y))
vertices.append((x, y - self.chamfer))
elif idx == 2:
vertices.append((x + self.chamfer, y))
vertices.append((x, y + self.chamfer))
elif idx == 3:
vertices.append((x - self.chamfer, y))
vertices.append((x, y + self.chamfer))
else:
vertices.append(corner)
self._vertices = [((x * self._cos_theta - y * self._sin_theta),
(x * self._sin_theta + y * self._cos_theta))
for x, y in vertices]
return self._vertices
@property
@ -1142,3 +1170,4 @@ class TestRecord(Primitive):
self.position = position
self.net_name = net_name
self.layer = layer
self._to_convert = ['position']

Wyświetl plik

@ -25,3 +25,4 @@ SVG is the only supported format.
from .cairo_backend import GerberCairoContext
from .render import RenderSettings

Wyświetl plik

@ -17,6 +17,7 @@
import cairocffi as cairo
import os
import tempfile
import copy
@ -36,16 +37,16 @@ class GerberCairoContext(GerberContext):
super(GerberCairoContext, self).__init__()
self.scale = (scale, scale)
self.surface = None
self.surface_buffer = None
self.ctx = None
self.active_layer = None
self.active_matrix = None
self.output_ctx = None
self.bg = False
self.mask = None
self.mask_ctx = None
self.has_bg = False
self.origin_in_inch = None
self.size_in_inch = None
self._xform_matrix = None
self._render_count = 0
@property
def origin_in_pixels(self):
@ -66,10 +67,8 @@ class GerberCairoContext(GerberContext):
self.size_in_inch = size_in_inch if self.size_in_inch is None else self.size_in_inch
if (self.surface is None) or new_surface:
self.surface_buffer = tempfile.NamedTemporaryFile()
self.surface = cairo.SVGSurface(
self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
self.surface = cairo.SVGSurface(self.surface_buffer, size_in_pixels[0], size_in_pixels[1])
self.output_ctx = cairo.Context(self.surface)
self.output_ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
self.output_ctx.scale(1, -1)
self.output_ctx.translate(-(origin_in_inch[0] * self.scale[0]),
(-origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
@ -77,20 +76,44 @@ class GerberCairoContext(GerberContext):
x0=-self.origin_in_pixels[0],
y0=self.size_in_pixels[1] + self.origin_in_pixels[1])
def render_layers(self, layers, filename, theme=THEMES['default']):
def render_layer(self, layer, filename=None, settings=None, bgsettings=None,
verbose=False):
if settings is None:
settings = THEMES['default'].get(layer.layer_class, RenderSettings())
if bgsettings is None:
bgsettings = THEMES['default'].get('background', RenderSettings())
if self._render_count == 0:
if verbose:
print('[Render]: Rendering Background.')
self.clear()
self.set_bounds(layer.bounds)
self._paint_background(bgsettings)
if verbose:
print('[Render]: Rendering {} Layer.'.format(layer.layer_class))
self._render_count += 1
self._render_layer(layer, settings)
if filename is not None:
self.dump(filename, verbose)
def render_layers(self, layers, filename, theme=THEMES['default'],
verbose=False):
""" Render a set of layers
"""
self.set_bounds(layers[0].bounds, True)
self._paint_background(True)
self.clear()
bgsettings = theme['background']
for layer in layers:
self._render_layer(layer, theme)
self.dump(filename)
settings = theme.get(layer.layer_class, RenderSettings())
self.render_layer(layer, settings=settings, bgsettings=bgsettings,
verbose=verbose)
self.dump(filename, verbose)
def dump(self, filename):
def dump(self, filename, verbose=False):
""" Save image as `filename`
"""
is_svg = filename.lower().endswith(".svg")
is_svg = os.path.splitext(filename.lower())[1] == '.svg'
if verbose:
print('[Render]: Writing image to {}'.format(filename))
if is_svg:
self.surface.finish()
self.surface_buffer.flush()
@ -115,30 +138,33 @@ class GerberCairoContext(GerberContext):
self.surface_buffer.flush()
return self.surface_buffer.read()
def _render_layer(self, layer, theme=THEMES['default']):
settings = theme.get(layer.layer_class, RenderSettings())
self.color = settings.color
self.alpha = settings.alpha
self.invert = settings.invert
def clear(self):
self.surface = None
self.output_ctx = None
self.has_bg = False
self.origin_in_inch = None
self.size_in_inch = None
self._xform_matrix = None
self._render_count = 0
if hasattr(self.surface_buffer, 'close'):
self.surface_buffer.close()
self.surface_buffer = None
def _render_layer(self, layer, settings):
self.invert = settings.invert
# Get a new clean layer to render on
self._new_render_layer(mirror=settings.mirror)
for prim in layer.primitives:
self.render(prim)
# Add layer to image
self._flatten()
self._paint(settings.color, settings.alpha)
def _render_line(self, line, color):
start = [pos * scale for pos, scale in zip(line.start, self.scale)]
end = [pos * scale for pos, scale in zip(line.end, self.scale)]
if not self.invert:
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if line.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
self.ctx.set_operator(cairo.OPERATOR_SOURCE
if line.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
if isinstance(line.aperture, Circle):
width = line.aperture.diameter
self.ctx.set_line_width(width * self.scale[0])
@ -162,14 +188,9 @@ class GerberCairoContext(GerberContext):
angle1 = arc.start_angle
angle2 = arc.end_angle
width = arc.aperture.diameter if arc.aperture.diameter != 0 else 0.001
if not self.invert:
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if arc.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
self.ctx.set_operator(cairo.OPERATOR_SOURCE
if arc.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
self.ctx.set_line_width(width * self.scale[0])
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
self.ctx.move_to(*start) # You actually have to do this...
@ -181,14 +202,9 @@ class GerberCairoContext(GerberContext):
self.ctx.move_to(*end) # ...lame
def _render_region(self, region, color):
if not self.invert:
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(cairo.OPERATOR_OVER
if region.level_polarity == 'dark'
else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
self.ctx.set_operator(cairo.OPERATOR_SOURCE
if region.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
self.ctx.set_line_width(0)
self.ctx.set_line_cap(cairo.LINE_CAP_ROUND)
self.ctx.move_to(*self.scale_point(region.primitives[0].start))
@ -210,29 +226,22 @@ class GerberCairoContext(GerberContext):
def _render_circle(self, circle, color):
center = self.scale_point(circle.position)
if not self.invert:
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(
cairo.OPERATOR_OVER if circle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
self.ctx.set_operator(cairo.OPERATOR_SOURCE
if circle.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
self.ctx.set_line_width(0)
self.ctx.arc(*center, radius=circle.radius *
self.scale[0], angle1=0, angle2=2 * math.pi)
self.ctx.arc(*center, radius=(circle.radius * self.scale[0]), angle1=0,
angle2=(2 * math.pi))
self.ctx.fill()
def _render_rectangle(self, rectangle, color):
lower_left = self.scale_point(rectangle.lower_left)
width, height = tuple([abs(coord) for coord in self.scale_point((rectangle.width, rectangle.height))])
if not self.invert:
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(
cairo.OPERATOR_OVER if rectangle.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
else:
self.ctx.set_source_rgba(0.0, 0.0, 0.0, 1.0)
self.ctx.set_operator(cairo.OPERATOR_CLEAR)
width, height = tuple([abs(coord) for coord in
self.scale_point((rectangle.width,
rectangle.height))])
self.ctx.set_operator(cairo.OPERATOR_SOURCE
if rectangle.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
self.ctx.set_line_width(0)
self.ctx.rectangle(*lower_left, width=width, height=height)
self.ctx.fill()
@ -247,34 +256,31 @@ class GerberCairoContext(GerberContext):
self._render_circle(circle, color)
def _render_test_record(self, primitive, color):
position = [pos + origin for pos, origin in zip(primitive.position, self.origin_in_inch)]
self.ctx.set_operator(cairo.OPERATOR_OVER)
position = [pos + origin for pos, origin in
zip(primitive.position, self.origin_in_inch)]
self.ctx.select_font_face(
'monospace', cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
self.ctx.set_font_size(13)
self._render_circle(Circle(position, 0.015), color)
self.ctx.set_source_rgba(*color, alpha=self.alpha)
self.ctx.set_operator(
cairo.OPERATOR_OVER if primitive.level_polarity == 'dark' else cairo.OPERATOR_CLEAR)
self.ctx.move_to(*[self.scale[0] * (coord + 0.015)
for coord in position])
self.ctx.set_operator(cairo.OPERATOR_SOURCE
if primitive.level_polarity == 'dark' and
(not self.invert) else cairo.OPERATOR_CLEAR)
self.ctx.move_to(*[self.scale[0] * (coord + 0.015) for coord in position])
self.ctx.scale(1, -1)
self.ctx.show_text(primitive.net_name)
self.ctx.scale(1, -1)
def _new_render_layer(self, color=None, mirror=False):
size_in_pixels = self.scale_point(self.size_in_inch)
matrix = copy.copy(self._xform_matrix)
layer = cairo.SVGSurface(None, size_in_pixels[0], size_in_pixels[1])
ctx = cairo.Context(layer)
ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
ctx.scale(1, -1)
ctx.translate(-(self.origin_in_inch[0] * self.scale[0]),
(-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
(-self.origin_in_inch[1] * self.scale[0]) - size_in_pixels[1])
if self.invert:
ctx.set_operator(cairo.OPERATOR_OVER)
ctx.set_source_rgba(*self.color, alpha=self.alpha)
ctx.paint()
matrix = copy.copy(self._xform_matrix)
if mirror:
matrix.xx = -1.0
matrix.x0 = self.origin_in_pixels[0] + self.size_in_pixels[0]
@ -282,21 +288,23 @@ class GerberCairoContext(GerberContext):
self.active_layer = layer
self.active_matrix = matrix
def _flatten(self):
self.output_ctx.set_operator(cairo.OPERATOR_OVER)
def _paint(self, color=None, alpha=None):
color = color if color is not None else self.color
alpha = alpha if alpha is not None else self.alpha
ptn = cairo.SurfacePattern(self.active_layer)
ptn.set_matrix(self.active_matrix)
self.output_ctx.set_source(ptn)
self.output_ctx.paint()
self.output_ctx.set_source_rgba(*color, alpha=alpha)
self.output_ctx.mask(ptn)
self.ctx = None
self.active_layer = None
self.active_matrix = None
def _paint_background(self, force=False):
if (not self.bg) or force:
self.bg = True
self.output_ctx.set_operator(cairo.OPERATOR_OVER)
self.output_ctx.set_source_rgba(*self.background_color, alpha=1.0)
def _paint_background(self, settings=None):
color = settings.color if settings is not None else self.background_color
alpha = settings.alpha if settings is not None else 1.0
if not self.has_bg:
self.has_bg = True
self.output_ctx.set_source_rgba(*color, alpha=alpha)
self.output_ctx.paint()
def scale_point(self, point):

Wyświetl plik

@ -45,7 +45,8 @@ class GerberContext(object):
Measurement units. 'inch' or 'metric'
color : tuple (<float>, <float>, <float>)
Color used for rendering as a tuple of normalized (red, green, blue) values.
Color used for rendering as a tuple of normalized (red, green, blue)
values.
drill_color : tuple (<float>, <float>, <float>)
Color used for rendering drill hits. Format is the same as for `color`.
@ -62,8 +63,9 @@ class GerberContext(object):
self._units = units
self._color = (0.7215, 0.451, 0.200)
self._background_color = (0.0, 0.0, 0.0)
self._drill_color = (0.0, 0.0, 0.0)
self._alpha = 1.0
self._invert = False
self.invert = False
self.ctx = None
@property
@ -125,14 +127,6 @@ class GerberContext(object):
raise ValueError('Alpha must be between 0.0 and 1.0')
self._alpha = alpha
@property
def invert(self):
return self._invert
@invert.setter
def invert(self, invert):
self._invert = invert
def render(self, primitive):
color = self.color
if isinstance(primitive, Line):
@ -156,7 +150,6 @@ class GerberContext(object):
else:
return
def _render_line(self, primitive, color):
pass
@ -186,8 +179,8 @@ class GerberContext(object):
class RenderSettings(object):
def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False, mirror=False):
def __init__(self, color=(0.0, 0.0, 0.0), alpha=1.0, invert=False,
mirror=False):
self.color = color
self.alpha = alpha
self.invert = invert

Wyświetl plik

@ -25,12 +25,12 @@ COLORS = {
'green': (0.0, 1.0, 0.0),
'blue': (0.0, 0.0, 1.0),
'fr-4': (0.290, 0.345, 0.0),
'green soldermask': (0.0, 0.612, 0.396),
'green soldermask': (0.0, 0.412, 0.278),
'blue soldermask': (0.059, 0.478, 0.651),
'red soldermask': (0.968, 0.169, 0.165),
'black soldermask': (0.298, 0.275, 0.282),
'purple soldermask': (0.2, 0.0, 0.334),
'enig copper': (0.686, 0.525, 0.510),
'enig copper': (0.694, 0.533, 0.514),
'hasl copper': (0.871, 0.851, 0.839)
}
@ -39,11 +39,11 @@ class Theme(object):
def __init__(self, name=None, **kwargs):
self.name = 'Default' if name is None else name
self.background = kwargs.get('background', RenderSettings(COLORS['black'], alpha=0.0))
self.background = kwargs.get('background', RenderSettings(COLORS['fr-4']))
self.topsilk = kwargs.get('topsilk', RenderSettings(COLORS['white']))
self.bottomsilk = kwargs.get('bottomsilk', RenderSettings(COLORS['white'], mirror=True))
self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True))
self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.8, invert=True, mirror=True))
self.topmask = kwargs.get('topmask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True))
self.bottommask = kwargs.get('bottommask', RenderSettings(COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True))
self.top = kwargs.get('top', RenderSettings(COLORS['hasl copper']))
self.bottom = kwargs.get('bottom', RenderSettings(COLORS['hasl copper'], mirror=True))
self.drill = kwargs.get('drill', RenderSettings(COLORS['black']))
@ -60,12 +60,21 @@ class Theme(object):
THEMES = {
'default': Theme(),
'OSH Park': Theme(name='OSH Park',
background=RenderSettings(COLORS['purple soldermask']),
top=RenderSettings(COLORS['enig copper']),
bottom=RenderSettings(COLORS['enig copper'], mirror=True),
topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True),
bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.8, invert=True, mirror=True)),
topmask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True),
bottommask=RenderSettings(COLORS['purple soldermask'], alpha=0.85, invert=True, mirror=True),
topsilk=RenderSettings(COLORS['white'], alpha=0.8),
bottomsilk=RenderSettings(COLORS['white'], alpha=0.8, mirror=True)),
'Blue': Theme(name='Blue',
topmask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True),
bottommask=RenderSettings(COLORS['blue soldermask'], alpha=0.8, invert=True)),
'Transparent Copper': Theme(name='Transparent',
background=RenderSettings((0.9, 0.9, 0.9)),
top=RenderSettings(COLORS['red'], alpha=0.5),
bottom=RenderSettings(COLORS['blue'], alpha=0.5),
drill=RenderSettings((0.3, 0.3, 0.3))),
}

Wyświetl plik

@ -48,7 +48,7 @@ def read(filename):
return GerberParser().parse(filename)
def loads(data):
def loads(data, filename=None):
""" Generate a GerberFile object from rs274x data in memory
Parameters
@ -56,12 +56,15 @@ def loads(data):
data : string
string containing gerber file contents
filename : string, optional
string containing the filename of the data source
Returns
-------
file : :class:`gerber.rs274x.GerberFile`
A GerberFile created from the specified file.
"""
return GerberParser().parse_raw(data)
return GerberParser().parse_raw(data, filename)
class GerberFile(CamFile):

Wyświetl plik

@ -14,7 +14,7 @@ IPC_D_356_FILE = os.path.join(os.path.dirname(__file__),
def test_read():
ipcfile = read(IPC_D_356_FILE)
assert(isinstance(ipcfile, IPC_D_356))
assert(isinstance(ipcfile, IPCNetlist))
def test_parser():

Wyświetl plik

@ -1,11 +1,33 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Hamilton Kibbe <ham@hamiltonkib.be>
# copyright 2016 Hamilton Kibbe <ham@hamiltonkib.be>
#
# 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.
import os
from .tests import *
from ..layers import guess_layer_class, hints
from ..layers import *
from ..common import read
NCDRILL_FILE = os.path.join(os.path.dirname(__file__),
'resources/ncdrill.DRD')
NETLIST_FILE = os.path.join(os.path.dirname(__file__),
'resources/ipc-d-356.ipc')
COPPER_FILE = os.path.join(os.path.dirname(__file__),
'resources/top_copper.GTL')
def test_guess_layer_class():
""" Test layer type inferred correctly from filename
@ -30,4 +52,51 @@ def test_guess_layer_class():
def test_sort_layers():
""" Test layer ordering
"""
pass
layers = [
PCBLayer(layer_class='drawing'),
PCBLayer(layer_class='drill'),
PCBLayer(layer_class='bottompaste'),
PCBLayer(layer_class='bottomsilk'),
PCBLayer(layer_class='bottommask'),
PCBLayer(layer_class='bottom'),
PCBLayer(layer_class='internal'),
PCBLayer(layer_class='top'),
PCBLayer(layer_class='topmask'),
PCBLayer(layer_class='topsilk'),
PCBLayer(layer_class='toppaste'),
PCBLayer(layer_class='outline'),
]
layer_order = ['outline', 'toppaste', 'topsilk', 'topmask', 'top',
'internal', 'bottom', 'bottommask', 'bottomsilk',
'bottompaste', 'drill', 'drawing']
bottom_order = list(reversed(layer_order[:10])) + layer_order[10:]
assert_equal([l.layer_class for l in sort_layers(layers)], layer_order)
assert_equal([l.layer_class for l in sort_layers(layers, from_top=False)],
bottom_order)
def test_PCBLayer_from_file():
layer = PCBLayer.from_cam(read(COPPER_FILE))
assert_true(isinstance(layer, PCBLayer))
layer = PCBLayer.from_cam(read(NCDRILL_FILE))
assert_true(isinstance(layer, DrillLayer))
layer = PCBLayer.from_cam(read(NETLIST_FILE))
assert_true(isinstance(layer, PCBLayer))
assert_equal(layer.layer_class, 'ipc_netlist')
def test_PCBLayer_bounds():
source = read(COPPER_FILE)
layer = PCBLayer.from_cam(source)
assert_equal(source.bounds, layer.bounds)
def test_DrillLayer_from_cam():
no_exceptions = True
try:
layer = DrillLayer.from_cam(read(NCDRILL_FILE))
assert_true(isinstance(layer, DrillLayer))
except:
no_exceptions = False
assert_true(no_exceptions)

Wyświetl plik

@ -291,9 +291,9 @@ def rotate_point(point, angle, center=(0.0, 0.0)):
`point` rotated about `center` by `angle` degrees.
"""
angle = radians(angle)
xdelta, ydelta = tuple(map(sub, point, center))
x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta)
y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta)
x_delta, y_delta = tuple(map(sub, point, center))
x = center[0] + (cos(angle) * x_delta) - (sin(angle) * y_delta)
y = center[1] + (sin(angle) * x_delta) - (cos(angle) * y_delta)
return (x, y)