initial commit

This commit is contained in:
opiopan 2019-03-21 22:00:32 +09:00
commit 9febca7da6
14 changed files with 1551 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.vscode
.python-version
*.pyc
__pycache__
pcb_tools_extension.egg-info

74
README.md Normal file
View file

@ -0,0 +1,74 @@
PCB tools extension
===
PCB tools extension is a Python library to panelize gerber files.
This library is designed based on [PCB tools](https://github.com/curtacircuitos/pcb-tools) which provides cool functionality to handle PCB such as generationg PCB image from gerber files.
PCB tools extension adds following function to PCB tools.
- Rotate PCB data (imprementation is not completed)
- Save loding PCB data
- Merge multiple PCB data
- Translate DXF file to gerber data
Only RS-274x format and Excellon drill format data can be handled by current version of this library.
## How to panelize
Following code is a example to panelize two top metal layer files.
``` python
import gerberex
ctx = gerberex.GerberComposition()
metal1 = gerberex.read('board1.gtl')
ctx.merge(metal1)
metal2 = gerberex.read('board2.gtl')
metal2.to_metric()
metal2.offset(30, 0)
ctx.merge(metal2)
ctx.dump('panelized-board.gtl')
```
In case of Excellon drill data, you have to use ```DrillCompositon``` instead of ```GerberComposition```.
```python
import gerberex
ctx = gerberex.DrillComposition()
drill1 = gerberex.read('board1.txt')
ctx.merge(drill1)
drill2 = gerberex.read('board2.txt')
drill2.to_metric()
drill2.offset(30, 0)
ctx.merge(drill2)
ctx.dump('panelized-board.txt')
```
## DXF file translation
You can also load a dxf file and handle that as same as RX-274x gerber file.<br>
This function is useful to generate outline data of pnanelized PCB boad.
```python
import gerberex
ctx = gerberex.GerberComposition()
dxf = gerberex.read('outline.dxf')
ctx.merge(dxf)
```
## Panelized board image Example
This image is generated by original [PCB tools](https://github.com/curtacircuitos/pcb-tools) fucntion.
<p align="center">
<img alt="description" src="https://raw.githubusercontent.com/wiki/opiopan/pcb-tools-extension/images/panelized.jpg" width=750>
</p>
## Installation
```shell
$ git clone https://github.com/opiopan/pcb-tools-extension.git
$ pip install pcb-tools-extension

8
gerberex/__init__.py Normal file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
from gerberex.common import (read, loads)
from gerberex.composition import (GerberComposition, DrillComposition)
from gerberex.dxf import DxfFile

183
gerberex/am_expression.py Normal file
View file

@ -0,0 +1,183 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
from gerber.utils import *
from gerber.am_eval import OpCode
from gerber.am_statements import *
class AMExpression(object):
CONSTANT = 1
VARIABLE = 2
OPERATOR = 3
def __init__(self, kind):
self.kind = kind
@property
def value(self):
return self
def optimize(self):
pass
def to_inch(self):
return AMOperatorExpression(AMOperatorExpression.DIV, self,
AMConstantExpression(MILLIMETERS_PER_INCH))
def to_metric(self):
return AMOperatorExpression(AMOperatorExpression.MUL, self,
AMConstantExpression(MILLIMETERS_PER_INCH))
def to_gerber(self, settings=None):
pass
def to_instructions(self):
pass
class AMConstantExpression(AMExpression):
def __init__(self, value):
super(AMConstantExpression, self).__init__(AMExpression.CONSTANT)
self._value = value
@property
def value(self):
return self._value
def optimize(self):
return self
def to_gerber(self, settings=None):
return str(self._value)
def to_instructions(self):
return [(OpCode.PUSH, self._value)]
class AMVariableExpression(AMExpression):
def __init__(self, number):
super(AMVariableExpression, self).__init__(AMExpression.VARIABLE)
self.number = number
def optimize(self):
return self
def to_gerber(self, settings=None):
return '$%d' % self.number
def to_instructions(self):
return (OpCode.LOAD, self.number)
class AMOperatorExpression(AMExpression):
ADD = '+'
SUB = '-'
MUL = 'X'
DIV = '/'
def __init__(self, op, lvalue, rvalue):
super(AMOperatorExpression, self).__init__(AMExpression.OPERATOR)
self.op = op
self.lvalue = lvalue
self.rvalue = rvalue
def optimize(self):
self.lvalue = self.lvalue.optimize()
self.rvalue = self.rvalue.optimize()
if isinstance(self.lvalue, AMConstantExpression) and isinstance(self.rvalue, AMConstantExpression):
lvalue = float(self.lvalue.value)
rvalue = float(self.rvalue.value)
value = lvalue + rvalue if self.op == self.ADD else \
lvalue - rvalue if self.op == self.SUB else \
lvalue * rvalue if self.op == self.MUL else \
lvalue / rvalue if self.op == self.DIV else None
return AMConstantExpression(value)
elif self.op == self.ADD:
if self.rvalue.value == 0:
return self.lvalue
elif self.lvalue.value == 0:
return self.rvalue
elif self.op == self.SUB:
if self.rvalue.value == 0:
return self.lvalue
elif self.lvalue.value == 0 and isinstance(self.rvalue, AMConstantExpression):
return AMConstantExpression(-self.rvalue.value)
elif self.op == self.MUL:
if self.rvalue.value == 1:
return self.lvalue
elif self.lvalue.value == 1:
return self.rvalue
elif self.lvalue == 0 or self.rvalue == 0:
return AMConstantExpression(0)
elif self.op == self.DIV:
if self.rvalue.value == 1:
return self.lvalue
elif self.lvalue.value == 0:
return AMConstantExpression(0)
return self
def to_gerber(self, settings=None):
return '(%s)%s(%s)' % (self.lvalue.to_gerber(settings), self.op, self.rvalue.to_gerber(settings))
def to_instructions(self):
for i in self.lvalue.to_instructions():
yield i
for i in self.rvalue.to_instructions():
yield i
op = OpCode.ADD if self.op == self.ADD else\
OpCode.SUB if self.op == self.SUB else\
OpCode.MUL if self.op == self.MUL else\
OpCode.DIV
yield (op, None)
def eval_macro(instructions):
stack = []
def pop():
return stack.pop()
def push(op):
stack.append(op)
def top():
return stack[-1]
def empty():
return len(stack) == 0
for opcode, argument in instructions:
if opcode == OpCode.PUSH:
push(AMConstantExpression(argument))
elif opcode == OpCode.LOAD:
push(AMVariableExpression(argument))
elif opcode == OpCode.STORE:
yield (-argument, [pop()])
elif opcode == OpCode.ADD:
op1 = pop()
op2 = pop()
push(AMOperatorExpression(AMOperatorExpression.ADD, op2, op1))
elif opcode == OpCode.SUB:
op1 = pop()
op2 = pop()
push(AMOperatorExpression(AMOperatorExpression.SUB, op2, op1))
elif opcode == OpCode.MUL:
op1 = pop()
op2 = pop()
push(AMOperatorExpression(AMOperatorExpression.MUL, op2, op1))
elif opcode == OpCode.DIV:
op1 = pop()
op2 = pop()
push(AMOperatorExpression(AMOperatorExpression.DIV, op2, op1))
elif opcode == OpCode.PRIM:
yield (argument, stack)
stack = []

437
gerberex/am_primitive.py Normal file
View file

@ -0,0 +1,437 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
from gerber.utils import *
from gerber.am_statements import *
from gerber.am_eval import OpCode
from gerberex.am_expression import eval_macro
class AMPrimitiveDef(AMPrimitive):
def __init__(self, code, exposure=None, rotation=0):
super(AMPrimitiveDef, self).__init__(code, exposure)
self.rotation = rotation
def to_inch(self):
pass
def to_metric(self):
pass
def to_gerber(self, settings=None):
pass
def to_instructions(self):
pass
class AMCommentPrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
return cls(code, modifiers[0])
def __init__(self, code, comment):
super(AMCommentPrimitiveDef, self).__init__(code)
self.comment = comment
def to_gerber(self, settings=None):
return '%d %s*' % (self.code, self.comment.to_gerber())
def to_instructions(self):
return [(OpCode.PUSH, self.comment), (OpCode.PRIM, self.code)]
class AMCirclePrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
exposure = 'on' if modifiers[0] == 1 else 'off',
diameter = modifiers[1],
center_x = modifiers[2],
center_y = modifiers[3],
rotation = modifiers[4]
return cls(code, expressions, center_x, center_y, rotation)
def __init__(self, code, exposure, diameter, center_x, center_y, rotation):
super(AMCirclePrimitiveDef, self).__init__(code, exposure, rotation)
self.diameter = diameter
self.center_x = center_x
self.center_y = center_y
def to_inch(self):
self.diameter = self.diameter.to_inch().optimize()
self.center_x = self.center_x.to_inch().optimize()
self.center_y = self.center_y.to_inch().optimize()
def to_metric(self):
self.diameter = self.diameter.to_metric().optimize()
self.center_x = self.center_x.to_metric().optimize()
self.center_y = self.center_y.to_metric().optimize()
def to_gerber(self, settings=None):
data = dict(code = self.code,
exposure = 1 if self.exposure == 'on' else 0,
diameter = self.diameter.to_gerber(settings),
x = self.center_x.to_gerber(settings),
y = self.center_y.to_gerber(settings),
rotation = self.rotation.to_gerber(settings))
return '{code},{exposure},{diameter},{x},{y},{rotation}*'.format(**data)
def to_instructions(self):
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
for modifier in [self.diameter, self.center_x, self.center_y, self.rotation]:
for i in modifier.to_instructions():
yield i
yield (OpCode.PRIM, self.code)
class AMVectorLinePrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
code = code
exposure = 'on' if modifiers[0] == 1 else 'off'
width = modifiers[1]
start_x = modifiers[2]
start_y = modifiers[3]
end_x = modifiers[4]
end_y = modifiers[5]
rotation = modifiers[6]
return cls(code, exposure, width, start_x, start_y, end_x, end_y, rotation)
def __init__(self, code, exposure, width, start_x, start_y, end_x, end_y, rotation):
super(AMVectorLinePrimitiveDef, self).__init__(code, exposure, rotation)
self.width = width
self.start_x = start_x
self.start_y = start_y
self.end_x = end_x
self.end_y = end_y
def to_inch(self):
self.width = self.width.to_inch().optimize()
self.start_x = self.start_x.to_inch().optimize()
self.start_y = self.start_y.to_inch().optimize()
self.end_x = self.end_x.to_inch().optimize()
self.end_y = self.end_x.to_inch().optimize()
def to_metric(self):
self.width = self.width.to_metric().optimize()
self.start_x = self.start_x.to_metric().optimize()
self.start_y = self.start_y.to_metric().optimize()
self.end_x = self.end_x.to_metric().optimize()
self.end_y = self.end_x.to_metric().optimize()
def to_gerber(self, settings=None):
data = dict(code = self.code,
exposure = 1 if self.exposure == 'on' else 0,
width = self.width.to_gerber(settings),
start_x = self.start_x.to_gerber(settings),
start_y = self.start_y.to_gerber(settings),
end_x = self.end_x.to_gerber(settings),
end_y = self.end_y.to_gerber(settings),
rotation = self.rotation.to_gerber(settings))
return '{code},{exposure},{width},{start_x},{start_y},{end_x},{end_y},{rotation}*'.format(**data)
def to_instructions(self):
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
modifiers = [self.width, self.start_x, self.start_y, self.end_x, self.end_y, self.rotation]
for modifier in modifiers:
for i in modifier.to_instructions():
yield i
yield (OpCode.PRIM, self.code)
class AMCenterLinePrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
code = code
exposure = 'on' if modifiers[0] == 1 else 'off'
width = modifiers[1]
height = modifiers[2]
x = modifiers[3]
y = modifiers[4]
rotation = modifiers[5]
return cls(code, exposure, width, height, x, y, rotation)
def __init__(self, code, exposure, width, height, x, y, rotation):
super(AMCenterLinePrimitiveDef, self).__init__(code, exposure, rotation)
self.width = width
self.height = height
self.x = x
self.y = y
def to_inch(self):
self.width = self.width.to_inch().optimize()
self.height = self.height.to_inch().optimize()
self.x = self.x.to_inch().optimize()
self.y = self.y.to_inch().optimize()
def to_metric(self):
self.width = self.width.to_metric().optimize()
self.height = self.height.to_metric().optimize()
self.x = self.x.to_metric().optimize()
self.y = self.y.to_metric().optimize()
def to_gerber(self, settings=None):
data = dict(code = self.code,
exposure = 1 if self.exposure == 'on' else 0,
width = self.width.to_gerber(settings),
height = self.height.to_gerber(settings),
x = self.x.to_gerber(settings),
y = self.y.to_gerber(settings),
rotation = self.rotation.to_gerber(settings))
return '{code},{exposure},{width},{height},{x},{y},{rotation}*'.format(**data)
def to_instructions(self):
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
modifiers = [self.width, self.height, self.x, self.y, self.rotation]
for modifier in modifiers:
for i in modifier.to_instructions():
yield i
yield (OpCode.PRIM, self.code)
class AMOutlinePrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
num_points = modifiers[1] + 1
code = code
exposure = 'on' if modifiers[0] == 1 else 'off'
addrs = modifiers[2:num_points * 2]
rotation = modifiers[3 + num_points * 2]
return cls(code, exposure, addrs, rotation)
def __init__(self, code, exposure, addrs, rotation):
super(AMOutlinePrimitiveDef, self).__init__(code, exposure, rotation)
self.addrs = addrs
def to_inch(self):
self.addrs = [i.to_inch() for i in self.addrs]
def to_metric(self):
self.addrs = [i.to_metric() for i in self.addrs]
def to_gerber(self, settings=None):
def strs():
yield '%d,%d,%d' % (self.code,
1 if self.exposure == 'on' else 0,
len(self.addrs) / 2 - 1)
for i in self.addrs:
yield i.to_gerber(settings)
yield self.rotation.to_gerber(settings)
return '%s*' % ','.join(strs())
def to_instructions(self):
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
yield (OpCode.PUSH, int(len(self.addrs) / 2 - 1))
for modifier in self.addrs:
for i in modifier.to_instructions():
yield i
for i in self.rotation.to_instructions():
yield i
yield (OpCode.PRIM, self.code)
class AMPolygonPrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
code = code
exposure = 'on' if modifiers[0] == 1 else 'off'
vertices = modifiers[1]
x = modifiers[2]
y = modifiers[3]
diameter = modifiers[4]
rotation = modifiers[5]
return cls(code, exposure, vertices, x, y, diameter, rotation)
def __init__(self, code, exposure, vertices, x, y, diameter, rotation):
super(AMPolygonPrimitiveDef, self).__init__(code, exposure, rotation)
self.vertices = vertices
self.x = x
self.y = y
self.diameter = diameter
def to_inch(self):
self.x = self.x.to_inch().optimize()
self.y = self.y.to_inch().optimize()
self.diameter = self.diameter.to_inch().optimize()
def to_metric(self):
self.x = self.x.to_metric().optimize()
self.y = self.y.to_metric().optimize()
self.diameter = self.diameter.to_inch().optimize()
def to_gerber(self, settings=None):
data = dict(code = self.code,
exposure = 1 if self.exposure == 'on' else 0,
vertices = self.vertices.to_gerber(settings),
x = self.x.to_gerber(settings),
y = self.y.to_gerber(settings),
diameter = self.diameter.to_gerber(settings),
rotation = self.rotation.to_gerber(settings))
return '{code},{exposure},{vertices},{x},{y},{diameter},{rotation}*'.format(**data)
def to_instructions(self):
yield (OpCode.PUSH, 1 if self.exposure == 'on' else 0)
modifiers = [self.vertices, self.x, self.y, self.diameter, self.rotation]
for modifier in modifiers:
for i in modifier.to_instructions():
yield i
yield (OpCode.PRIM, self.code)
class AMMoirePrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
code = code
exposure = 'on'
x = modifiers[0]
y = modifiers[1]
diameter = modifiers[2]
ring_thickness = modifiers[3]
gap = modifiers[4]
max_rings = modifiers[5]
crosshair_thickness = modifiers[6]
crosshair_length = modifiers[7]
rotation = modifiers[8]
return cls(code, exposure, x, y, diameter, ring_thickness, gap,
max_rings, crosshair_thickness, crosshair_length, rotation)
def __init__(self, code, exposure, x, y, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation):
super(AMMoirePrimitiveDef, self).__init__(code, exposure, rotation)
self.x = x
self.y = y
self.diameter = diameter
self.ring_thickness = ring_thickness
self.gap = gap
self.max_rings = max_rings
self.crosshair_thickness = crosshair_thickness
self.crosshair_length = crosshair_length
def to_inch(self):
self.x = self.x.to_inch().optimize()
self.y = self.y.to_inch().optimize()
self.diameter = self.diameter.to_inch().optimize()
self.ring_thickness = self.ring_thickness.to_inch().optimize()
self.gap = self.gap.to_inch().optimize()
self.crosshair_thickness = self.crosshair_thickness.to_inch().optimize()
self.crosshair_length = self.crosshair_length.to_inch().optimize()
def to_metric(self):
self.x = self.x.to_metric().optimize()
self.y = self.y.to_metric().optimize()
self.diameter = self.diameter.to_metric().optimize()
self.ring_thickness = self.ring_thickness.to_metric().optimize()
self.gap = self.gap.to_metric().optimize()
self.crosshair_thickness = self.crosshair_thickness.to_metric().optimize()
self.crosshair_length = self.crosshair_length.to_metric().optimize()
def to_gerber(self, settings=None):
data = dict(code = self.code,
x = self.x.to_gerber(settings),
y = self.y.to_gerber(settings),
diameter = self.diameter.to_gerber(settings),
ring_thickness = self.ring_thickness.to_gerber(settings),
gap = self.gap.to_gerber(settings),
max_rings = self.max_rings.to_gerber(settings),
crosshair_thickness = self.crosshair_thickness.to_gerber(settings),
crosshair_length = self.crosshair_length.to_gerber(settings),
rotation = self.rotation.to_gerber(settings))
return '{code},{x},{y},{diameter},{ring_thickness},{gap},{max_rings},'\
'{crosshair_thickness},{crosshair_length},{rotation}*'.format(**data)
def to_instructions(self):
modifiers = [self.x, self.y, self.diameter,
self.ring_thickness, self.gap, self.max_rings,
self.crosshair_thickness, self.crosshair_length,
self.rotation]
for modifier in modifiers:
for i in modifier.to_instructions():
yield i
yield (OpCode.PRIM, self.code)
class AMThermalPrimitiveDef(AMPrimitiveDef):
@classmethod
def from_modifiers(cls, code, modifiers):
code = code
exposure = 'on'
x = modifiers[0]
y = modifiers[1]
outer_diameter = modifiers[2]
inner_diameter = modifiers[3]
gap = modifiers[4]
rotation = modifiers[5]
return cls(code, exposure, x, y, outer_diameter, inner_diameter, gap, rotation)
def __init__(self, code, exposure, x, y, outer_diameter, inner_diameter, gap, rotation):
super(AMThermalPrimitiveDef, self).__init__(code, exposure, rotation)
self.x = x
self.y = y
self.outer_diameter = outer_diameter
self.inner_diameter = inner_diameter
self.gap = gap
def to_inch(self):
self.x = self.x.to_inch().optimize()
self.y = self.y.to_inch().optimize()
self.outer_diameter = self.outer_diameter.to_inch().optimize()
self.inner_diameter = self.inner_diameter.to_inch().optimize()
self.gap = self.gap.to_inch().optimize()
def to_metric(self):
self.x = self.x.to_metric().optimize()
self.y = self.y.to_metric().optimize()
self.outer_diameter = self.outer_diameter.to_metric().optimize()
self.inner_diameter = self.inner_diameter.to_metric().optimize()
self.gap = self.gap.to_metric().optimize()
def to_gerber(self, settings=None):
data = dict(code = self.code,
x = self.x.to_gerber(settings),
y = self.y.to_gerber(settings),
outer_diameter = self.outer_diameter.to_gerber(settings),
inner_diameter = self.inner_diameter.to_gerber(settings),
gap = self.gap.to_gerber(settings),
rotation = self.rotation.to_gerber(settings))
return '{code},{x},{y},{outer_diameter},{inner_diameter},'\
'{gap},{rotation}*'.format(**data)
def to_instructions(self):
modifiers = [self.x, self.y, self.outer_diameter,
self.inner_diameter, self.gap, self.rotation]
for modifier in modifiers:
for i in modifier.to_instructions():
yield i
yield (OpCode.PRIM, self.code)
class AMVariableDef(object):
def __init__(self, number, value):
self.number = number
self.value = value
def to_inch(self):
return self
def to_metric(self):
return self
def to_gerber(self, settings=None):
return '$%d=%s*' % (self.number, self.value.to_gerber(settings))
def to_instructions(self):
for i in self.value.to_instructions():
yield i
yield (OpCode.STORE, self.number)
def to_primitive_defs(instructions):
classes = {
0: AMCommentPrimitiveDef,
1: AMCirclePrimitiveDef,
2: AMVectorLinePrimitiveDef,
20: AMVectorLinePrimitiveDef,
21: AMCenterLinePrimitiveDef,
4: AMOutlinePrimitiveDef,
5: AMPolygonPrimitiveDef,
6: AMMoirePrimitiveDef,
7: AMThermalPrimitiveDef,
}
for code, modifiers in eval_macro(instructions):
if code < 0:
yield AMVariableDef(-code, modifiers[0])
else:
primitive = classes[code]
yield primitive.from_modifiers(code, modifiers)

35
gerberex/common.py Normal file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
import os
from gerber.common import loads as loads_org
from gerber.exceptions import ParseError
from gerber.utils import detect_file_format
import gerber.rs274x
import gerber.ipc356
import gerberex.rs274x
import gerberex.excellon
import gerberex.dxf
def read(filename, format=None):
with open(filename, 'rU') as f:
data = f.read()
return loads(data, filename, format=format)
def loads(data, filename=None, format=None):
if os.path.splitext(filename if filename else '')[1].lower() == '.dxf':
return gerberex.dxf.loads(data, filename)
fmt = detect_file_format(data)
if fmt == 'rs274x':
file = gerber.rs274x.loads(data, filename=filename)
return gerberex.rs274x.GerberFile.from_gerber_file(file)
elif fmt == 'excellon':
return gerberex.excellon.loads(data, filename=filename, format=format)
elif fmt == 'ipc_d_356':
return ipc356.loads(data, filename=filename)
else:
raise ParseError('Unable to detect file format')

201
gerberex/composition.py Normal file
View file

@ -0,0 +1,201 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
import os
from functools import reduce
from gerber.cam import FileSettings
from gerber.gerber_statements import EofStmt
from gerber.excellon_statements import *
import gerberex.rs274x
import gerberex.excellon
import gerberex.dxf
class Composition(object):
def __init__(self, settings = None, comments = None):
self.settings = settings
self.comments = comments if comments != None else []
class GerberComposition(Composition):
APERTURE_ID_BIAS = 10
def __init__(self, settings=None, comments=None):
super(GerberComposition, self).__init__(settings, comments)
self.param_statements = []
self.aperture_macros = {}
self.apertures = []
self.drawings = []
def merge(self, file):
if isinstance(file, gerberex.rs274x.GerberFile):
self._merge_gerber(file)
elif isinstance(file, gerberex.dxf.DxfFile):
self._merge_dxf(file)
else:
raise Exception('unsupported file type')
def dump(self, path):
def statements():
for s in self.param_statements:
yield s
for k in self.aperture_macros:
yield self.aperture_macros[k]
for s in self.apertures:
yield s
for s in self.drawings:
yield s
yield EofStmt()
with open(path, 'w') as f:
for statement in statements():
f.write(statement.to_gerber(self.settings) + '\n')
def _merge_gerber(self, file):
param_statements = []
aperture_macro_map = {}
aperture_map = {}
if self.settings:
if self.settings.units == 'metric':
file.to_metric()
else:
file.to_inch()
for statement in file.statements:
if statement.type == 'COMMENT':
self.comments.append(statement.comment)
elif statement.type == 'PARAM':
if statement.param == 'AM':
name = statement.name
newname = self._register_aperture_macro(statement)
aperture_macro_map[name] = newname
elif statement.param == 'AD':
if not statement.shape in ['C', 'R', 'O']:
statement.shape = aperture_macro_map[statement.shape]
dnum = statement.d
newdnum = self._register_aperture(statement)
aperture_map[dnum] = newdnum
elif statement.param == 'LP':
self.drawings.append(statement)
else:
param_statements.append(statement)
elif statement.type in ['EOF', "DEPRECATED"]:
pass
else:
if statement.type == 'APERTURE':
statement.d = aperture_map[statement.d]
self.drawings.append(statement)
if not self.settings:
self.settings = file.settings
self.param_statements = param_statements
def _merge_dxf(self, file):
if self.settings:
if self.settings.units == 'metric':
file.to_metric()
else:
file.to_inch()
file.dcode = self._register_aperture(file.aperture)
self.drawings.append(file.statements)
if not self.settings:
self.settings = file.settings
self.param_statements = file.header
def _register_aperture_macro(self, statement):
name = statement.name
newname = name
offset = 0
while newname in self.aperture_macros:
offset += 1
newname = '%s_%d' % (name, offset)
statement.name = newname
self.aperture_macros[newname] = statement
return newname
def _register_aperture(self, statement):
statement.d = len(self.apertures) + self.APERTURE_ID_BIAS
self.apertures.append(statement)
return statement.d
class DrillComposition(Composition):
def __init__(self, settings=None, comments=None):
super(DrillComposition, self).__init__(settings, comments)
self.header1_statements = []
self.header2_statements = []
self.tools = []
self.hits = []
def merge(self, file):
if isinstance(file, gerberex.excellon.ExcellonFileEx):
self._merge_excellon(file)
else:
raise Exception('unsupported file type')
def dump(self, path):
def statements():
for s in self.header1_statements:
yield s.to_excellon(self.settings)
for t in self.tools:
yield t.to_excellon(self.settings)
for s in self.header2_statements:
yield s.to_excellon(self.settings)
for t in self.tools:
yield ToolSelectionStmt(t.number).to_excellon(self.settings)
for h in self.hits:
if h.tool.number == t.number:
yield CoordinateStmt(*h.position).to_excellon(self.settings)
yield EndOfProgramStmt().to_excellon()
with open(path, 'w') as f:
for statement in statements():
f.write(statement + '\n')
def _merge_excellon(self, file):
tool_map = {}
if not self.settings:
self.settings = file.settings
else:
if self.settings.units == 'metric':
file.to_metric()
else:
file.to_inch()
if not self.header1_statements:
in_header1 = True
for statement in file.statements:
if not isinstance(statement, ToolSelectionStmt):
if isinstance(statement, ExcellonTool):
in_header1 = False
else:
if in_header1:
self.header1_statements.append(statement)
else:
self.header2_statements.append(statement)
else:
break
for tool in iter(file.tools.values()):
num = tool.number
tool_map[num] = self._register_tool(tool)
for hit in file.hits:
hit.tool = tool_map[hit.tool.number]
self.hits.append(hit)
def _register_tool(self, tool):
for existing in self.tools:
if existing.equivalent(tool):
return existing
new_tool = ExcellonTool.from_tool(tool)
new_tool.settings = self.settings
def toolnums():
for tool in self.tools:
yield tool.number
max_num = reduce(lambda x, y: x if x > y else y, toolnums(), 0)
new_tool.number = max_num + 1
self.tools.append(new_tool)
return new_tool

357
gerberex/dxf.py Normal file
View file

@ -0,0 +1,357 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
import io
from math import pi, cos, sin, tan, atan, atan2, acos, asin, sqrt
from gerber.cam import CamFile, FileSettings
from gerber.utils import inch, metric, write_gerber_value
from gerber.gerber_statements import ADParamStmt
import dxfgrabber
class DxfStatement(object):
def __init__(self, entity):
self.entity = entity
def to_gerber(self, settings=None):
pass
def to_inch(self):
pass
def to_metric(self):
pass
class DxfLineStatement(DxfStatement):
def __init__(self, entity):
super(DxfLineStatement, self).__init__(entity)
def to_gerber(self, settings=FileSettings):
x0 = self.entity.start[0]
y0 = self.entity.start[1]
x1 = self.entity.end[0]
y1 = self.entity.end[1]
return 'G01*\nX{0}Y{1}D02*\nX{2}Y{3}D01*'.format(
write_gerber_value(x0, settings.format,
settings.zero_suppression),
write_gerber_value(y0, settings.format,
settings.zero_suppression),
write_gerber_value(x1, settings.format,
settings.zero_suppression),
write_gerber_value(y1, settings.format,
settings.zero_suppression)
)
def to_inch(self):
self.entity.start[idx] = (
inch(self.entity.start[idx][0]), inch(self.entity.start[idx][1]))
self.entity.end[idx] = (
inch(self.entity.end[idx][0]), inch(self.entity.end[idx][1]))
def to_metric(self):
self.entity.start[idx] = (
metric(self.entity.start[idx][0]), inch(self.entity.start[idx][1]))
self.entity.end[idx] = (
metric(self.entity.end[idx][0]), inch(self.entity.end[idx][1]))
class DxfCircleStatement(DxfStatement):
def __init__(self, entity):
super(DxfCircleStatement, self).__init__(entity)
def to_gerber(self, settings=FileSettings):
r = self.entity.radius
x0 = self.entity.center[0]
y0 = self.entity.center[1]
return 'G01*\nX{0}Y{1}D02*\n' \
'G75*\nG03*\nX{2}Y{3}I{4}J{5}D01*'.format(
write_gerber_value(x0 + r, settings.format,
settings.zero_suppression),
write_gerber_value(y0, settings.format,
settings.zero_suppression),
write_gerber_value(x0 + r, settings.format,
settings.zero_suppression),
write_gerber_value(y0, settings.format,
settings.zero_suppression),
write_gerber_value(-r, settings.format,
settings.zero_suppression),
write_gerber_value(0, settings.format,
settings.zero_suppression)
)
def to_inch(self):
self.entity.radius = inch(self.entity.radius)
self.entity.center[idx] = (
inch(self.entity.center[idx][0]), inch(self.entity.center[idx][1]))
def to_metric(self):
self.entity.radius = metric(self.entity.radius)
self.entity.center[idx] = (
metric(self.entity.center[idx][0]), metric(self.entity.center[idx][1]))
class DxfArcStatement(DxfStatement):
def __init__(self, entity):
super(DxfArcStatement, self).__init__(entity)
def to_gerber(self, settings=FileSettings):
deg0 = self.entity.start_angle
deg1 = self.entity.end_angle
r = self.entity.radius
x0 = self.entity.center[0]
y0 = self.entity.center[1]
begin_x = x0 + r * cos(deg0 / 180. * pi)
begin_y = y0 + r * sin(deg0 / 180. * pi)
end_x = x0 + r * cos(deg1 / 180. * pi)
end_y = y0 + r * sin(deg1 / 180. * pi)
return 'G01*\nX{0}Y{1}D02*\n' \
'G75*\nG{2}*\nX{3}Y{4}I{5}J{6}D01*'.format(
write_gerber_value(begin_x, settings.format,
settings.zero_suppression),
write_gerber_value(begin_y, settings.format,
settings.zero_suppression),
'03' if deg0 > deg1 else '02',
write_gerber_value(end_x, settings.format,
settings.zero_suppression),
write_gerber_value(end_y, settings.format,
settings.zero_suppression),
write_gerber_value(x0 - begin_x, settings.format,
settings.zero_suppression),
write_gerber_value(y0 - begin_y, settings.format,
settings.zero_suppression)
)
def to_inch(self):
self.entity.start_angle = inch(self.entity.start_angle)
self.entity.end_angle = inch(self.entity.end_angle)
self.entity.radius = inch(self.entity.radius)
self.entity.center[idx] = (
inch(self.entity.center[idx][0]), inch(self.entity.center[idx][1]))
def to_metric(self):
self.entity.start_angle = metric(self.entity.start_angle)
self.entity.end_angle = metric(self.entity.end_angle)
self.entity.radius = metric(self.entity.radius)
self.entity.center[idx] = (
metric(self.entity.center[idx][0]), metric(self.entity.center[idx][1]))
class DxfPolylineStatement(DxfStatement):
def __init__(self, entity):
super(DxfPolylineStatement, self).__init__(entity)
def to_gerber(self, settings=FileSettings()):
x0 = self.entity.points[0][0]
y0 = self.entity.points[0][1]
b = self.entity.bulge[0]
gerber = 'G01*\nX{0}Y{1}D02*\nG75*'.format(
write_gerber_value(x0, settings.format,
settings.zero_suppression),
write_gerber_value(y0, settings.format,
settings.zero_suppression),
)
def ptseq():
for i in range(1, len(self.entity.points)):
yield i
if self.entity.is_closed:
yield 0
for idx in ptseq():
pt = self.entity.points[idx]
x1 = pt[0]
y1 = pt[1]
if b == 0:
gerber += '\nG01*\nX{0}Y{1}D01*'.format(
write_gerber_value(x1, settings.format,
settings.zero_suppression),
write_gerber_value(y1, settings.format,
settings.zero_suppression),
)
else:
ang = 4 * atan(b)
xm = x0 + x1
ym = y0 + y1
t = 1 / tan(ang / 2)
xc = (xm - t * (y1 - y0)) / 2
yc = (ym + t * (x1 - x0)) / 2
r = sqrt((x0 - xc)*(x0 - xc) + (y0 - yc)*(y0 - yc))
gerber += '\nG{0}*\nX{1}Y{2}I{3}J{4}D01*'.format(
'03' if ang > 0 else '02',
write_gerber_value(x1, settings.format,
settings.zero_suppression),
write_gerber_value(y1, settings.format,
settings.zero_suppression),
write_gerber_value(xc - x0, settings.format,
settings.zero_suppression),
write_gerber_value(yc - y0, settings.format,
settings.zero_suppression)
)
x0 = x1
y0 = y1
b = self.entity.bulge[idx]
return gerber
def to_inch(self):
for idx in range(0, len(self.entity.points)):
self.entity.points[idx] = (
inch(self.entity.points[idx][0]), inch(self.entity.points[idx][1]))
self.entity.bulge[idx] = inch(self.entity.bulge[idx])
def to_metric(self):
for idx in range(0, len(self.entity.points)):
self.entity.points[idx] = (
metric(self.entity.points[idx][0]), metric(self.entity.points[idx][1]))
self.entity.bulge[idx] = metric(self.entity.bulge[idx])
class DxfStatements(object):
def __init__(self, entities, units, dcode=10, draw_mode=None):
if draw_mode == None:
draw_mode = DxfFile.DM_LINE
self._units = units
self.dcode = dcode
self.draw_mode = draw_mode
self.statements = []
for entity in entities:
if entity.dxftype == 'LWPOLYLINE':
self.statements.append(DxfPolylineStatement(entity))
elif entity.dxftype == 'LINE':
self.statements.append(DxfLineStatement(entity))
elif entity.dxftype == 'CIRCLE':
self.statements.append(DxfCircleStatement(entity))
elif entity.dxftype == 'ARC':
self.statements.append(DxfArcStatement(entity))
@property
def units(self):
return _units
def to_gerber(self, settings=FileSettings()):
def gerbers():
yield 'D{0}*'.format(self.dcode)
if self.draw_mode == DxfFile.DM_FILL:
yield 'G36*'
for statement in self.statements:
if isinstance(statement, DxfCircleStatement) or \
(isinstance(statement, DxfPolylineStatement) and statement.entity.is_closed):
yield statement.to_gerber(settings)
yield 'G37*'
else:
for statement in self.statements:
yield statement.to_gerber(settings)
return '\n'.join(gerbers())
def to_inch(self):
if self._units == 'metric':
self._units = 'inch'
for statement in self.statements:
statement.to_inch()
def to_metric(self):
if self._units == 'inch':
self._units = 'metric'
for statement in self.statements:
statement.to_metric()
class DxfHeaderStatement(object):
def to_gerber(self, settings):
return 'G75*\n'\
'%MO{0}*%\n'\
'%OFA0B0*%\n'\
'%FS{1}AX{2}{3}Y{4}{5}*%\n'\
'%IPPOS*%\n'\
'%LPD*%'.format(
'IN' if settings.units == 'inch' else 'MM',
'L' if settings.zero_suppression == 'leading' else 'T',
settings.format[0], settings.format[1],
settings.format[0], settings.format[1]
)
def to_inch(self):
pass
def to_metric(self):
pass
class DxfFile(CamFile):
DM_LINE = 0
DM_FILL = 1
def __init__(self, dxf, settings=FileSettings(), draw_mode=None, filename=None):
if draw_mode == None:
draw_mode = self.DM_LINE
if dxf.header['$INSUNITS'] == 1:
settings.units = 'inch'
settings.format = (2, 5)
else:
settings.units = 'metric'
settings.format = (3, 4)
super(DxfFile, self).__init__(settings=settings, filename=filename)
self._draw_mode = draw_mode
self.header = DxfHeaderStatement()
self.aperture = ADParamStmt.circle(dcode=10, diameter=0.0)
self.statements = DxfStatements(dxf.entities, self.units, dcode=self.aperture.d, draw_mode=self.draw_mode)
@property
def dcode(self):
return self.aperture.dcode
@dcode.setter
def dcode(self, value):
self.aperture.d = value
self.statements.dcode = value
@property
def width(self):
return self.aperture.modifiers[0][0]
@width.setter
def width(self, value):
self.aperture.modifiers = ([float(value),],)
@property
def draw_mode(self):
return self._draw_mode
@draw_mode.setter
def draw_mode(self, value):
self._draw_mode = value
self.statements.draw_mode = value
def write(self, filename=None):
if self.settings.notation != 'absolute':
raise Exception('DXF file\'s notation must be absolute ')
filename = filename if filename is not None else self.filename
with open(filename, 'w') as f:
f.write(self.header.to_gerber(self.settings) + '\n')
f.write(self.aperture.to_gerber(self.settings) + '\n')
f.write(self.statements.to_gerber(self.settings) + '\n')
f.write('M02*\n')
def to_inch(self):
if self.units == 'metric':
self.header.to_inch()
self.aperture.to_inch()
self.statements.to_inch()
self.units = 'inch'
def to_metric(self):
if self.units == 'inch':
self.header.to_metric()
self.aperture.to_metric()
self.statements.to_metric()
self.units = 'metric'
def offset(self, ofset_x, offset_y):
raise Exception('Not supported')
def loads(data, filename=None):
stream = io.StringIO(data)
dxf = dxfgrabber.read(stream)
return DxfFile(dxf)

42
gerberex/excellon.py Normal file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
from gerber.excellon import (ExcellonParser, detect_excellon_format, ExcellonFile)
from gerber.excellon_statements import UnitStmt
from gerber.cam import FileSettings
def loads(data, filename=None, settings=None, tools=None, format=None):
if not settings:
settings = FileSettings(**detect_excellon_format(data))
if format:
settings.format = format
file = ExcellonParser(settings, tools).parse_raw(data, filename)
return ExcellonFileEx.from_file(file)
class ExcellonFileEx(ExcellonFile):
@classmethod
def from_file(cls, file):
statements = [
UnitStmtEx.from_statement(s) if isinstance(s, UnitStmt) else s \
for s in file.statements
]
return cls(statements, file.tools, file.hits, file.settings, file.filename)
def __init__(self, statements, tools, hits, settings, filename=None):
super(ExcellonFileEx, self).__init__(statements, tools, hits, settings, filename)
class UnitStmtEx(UnitStmt):
@classmethod
def from_statement(cls, stmt):
return cls(units=stmt.units, zeros=stmt.zeros, format=stmt.format, id=stmt.id)
def __init__(self, units='inch', zeros='leading', format=None, **kwargs):
super(UnitStmtEx, self).__init__(units, zeros, format, **kwargs)
def to_excellon(self, settings=None):
stmt = '%s,%s,%s.%s' % ('INCH' if self.units == 'inch' else 'METRIC',
'LZ' if self.zeros == 'leading' else 'TZ',
'0' * self.format[0], '0' * self.format[1])
return stmt

25
gerberex/rs274x.py Normal file
View file

@ -0,0 +1,25 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
import gerber.rs274x
from gerberex.statements import (AMParamStmt, AMParamStmtEx)
class GerberFile(gerber.rs274x.GerberFile):
@classmethod
def from_gerber_file(cls, gerber_file):
if not isinstance(gerber_file, gerber.rs274x.GerberFile):
raise Exception('only gerber.rs274x.GerberFile object is specified')
def swap_statement(statement):
if isinstance(statement, AMParamStmt) and not isinstance(statement, AMParamStmtEx):
return AMParamStmtEx.from_stmt(statement)
else:
return statement
statements = [swap_statement(statement) for statement in gerber_file.statements]
return cls(statements, gerber_file.settings, gerber_file.primitives,\
gerber_file.apertures, gerber_file.filename)
def __init__(self, statements, settings, primitives, apertures, filename=None):
super(GerberFile, self).__init__(statements, settings, primitives, apertures, filename)

34
gerberex/statements.py Normal file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
from gerber.gerber_statements import AMParamStmt
from gerberex.am_primitive import to_primitive_defs
class AMParamStmtEx(AMParamStmt):
@classmethod
def from_stmt(cls, stmt):
return cls(stmt.param, stmt.name, stmt.macro)
def __init__(self, param, name, macro):
super(AMParamStmtEx, self).__init__(param, name, macro)
self.primitive_defs = to_primitive_defs(self.instructions)
def to_inch(self):
if self.units == 'metric':
self.units = 'inch'
for p in self.primitive_defs:
p.to_inch()
def to_metric(self):
if self.units == 'inch':
self.units = 'metric'
for p in self.primitive_defs:
p.to_metric()
def to_gerber(self, settings = None):
def plist():
for p in self.primitive_defs:
yield p.to_gerber(settings)
return "%%AM%s*\n%s%%" % (self.name, '\n'.join(plist()))

49
setup.py Normal file
View file

@ -0,0 +1,49 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2013-2014 Paulo Henrique Silva <ph.silva@gmail.com>
# 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
import os
METADATA = {
'name': 'pcb-tools-extension',
'version': 0.1,
'author': 'Hiroshi Murayama <opiopan@gmail.com>',
'author_email': "opiopan@gmail.com",
'description': ("Extension for pcb-tools package to panelize gerber files"),
'license': "Apache",
'keywords': "pcb gerber tools extension",
'url': "http://github.com/opiopan/pcb-tools-extension",
'packages': ['gerberex'],
'classifiers':[
"Development Status :: 3 - Alpha",
"Topic :: Utilities",
"License :: OSI Approved :: Apple Public Source License",
],
}
SETUPTOOLS_METADATA = {
'install_requires': ['pcb-tools', 'dxfgrabber'],
}
def install():
""" Install using setuptools, fallback to distutils
"""
try:
from setuptools import setup
METADATA.update(SETUPTOOLS_METADATA)
setup(**METADATA)
except ImportError:
from sys import stderr
stderr.write('Could not import setuptools, using distutils')
stderr.write('NOTE: You will need to install dependencies manualy')
from distutils.core import setup
setup(**METADATA)
if __name__ == '__main__':
install()

38
test/panelimage.py Normal file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env python
from gerber import load_layer
from gerber.render import RenderSettings, theme
from gerber.render.cairo_backend import GerberCairoContext
print('loading ', end='', flush=True)
copper = load_layer('panelized.GTL')
print('.', end='', flush=True)
mask = load_layer('panelized.GTS')
print('.', end='', flush=True)
silk = load_layer('panelized.GTO')
print('.', end='', flush=True)
drill = load_layer('panelized.TXT')
print('.', end='', flush=True)
outline = load_layer('panelized-fill.GML')
print('.', end='', flush=True)
print('. end', flush=True)
print('panelizing ', end='', flush=True)
ctx = GerberCairoContext(scale=30)
print('.', end='', flush=True)
ctx.render_layer(copper)
print('.', end='', flush=True)
ctx.render_layer(mask)
print('.', end='', flush=True)
our_settings = RenderSettings(color=theme.COLORS['white'], alpha=0.85)
ctx.render_layer(silk, settings=our_settings)
print('.', end='', flush=True)
ctx.render_layer(outline)
print('.', end='', flush=True)
ctx.render_layer(drill)
print('.', end='', flush=True)
print('. end', flush=True)
print('dumping top...')
ctx.dump('panelized.png')

63
test/test.py Normal file
View file

@ -0,0 +1,63 @@
import gerberex
from gerberex.dxf import DxfFile
import gerber
from gerber.render.cairo_backend import GerberCairoContext
def merge():
ctx = gerberex.GerberComposition()
a = gerberex.read('test.GTL')
a.to_metric()
ctx.merge(a)
b = gerberex.read('test.GTL')
b.to_metric()
b.offset(0, 25)
ctx.merge(b)
c = gerberex.read('test2.GTL')
c.to_metric()
c.offset(0, 60)
ctx.merge(c)
c = gerberex.read('test.GML')
c.to_metric()
ctx.merge(c)
ctx.dump('test-merged.GTL')
def merge2():
ctx = gerberex.DrillComposition()
a = gerberex.read('test.TXT')
a.to_metric()
ctx.merge(a)
b = gerberex.read('test.TXT')
b.to_metric()
b.offset(0, 25)
ctx.merge(b)
c = gerberex.read('test2.TXT')
c.to_metric()
c.offset(0, 60)
ctx.merge(c)
ctx.dump('test-merged.TXT')
#merge2()
file = gerberex.read('outline.dxf')
file.to_metric()
w = file.width
file.draw_mode = DxfFile.DM_FILL
file.write('outline.GML')
copper = gerber.load_layer('test-merged.GTL')
ctx = GerberCairoContext(scale=10)
ctx.render_layer(copper)
outline = gerber.load_layer('test.GML')
outline.cam_source.to_metric()
ctx.render_layer(outline)
drill = gerber.load_layer('test-merged.TXT')
ctx.render_layer(drill)
ctx.dump('test.png')