aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--README.md74
-rw-r--r--gerberex/__init__.py8
-rw-r--r--gerberex/am_expression.py183
-rw-r--r--gerberex/am_primitive.py437
-rw-r--r--gerberex/common.py35
-rw-r--r--gerberex/composition.py201
-rw-r--r--gerberex/dxf.py357
-rw-r--r--gerberex/excellon.py42
-rw-r--r--gerberex/rs274x.py25
-rw-r--r--gerberex/statements.py34
-rw-r--r--setup.py49
-rw-r--r--test/panelimage.py38
-rw-r--r--test/test.py63
14 files changed, 1551 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b70e393
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+.vscode
+.python-version
+*.pyc
+__pycache__
+pcb_tools_extension.egg-info
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e3fb731
--- /dev/null
+++ b/README.md
@@ -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 \ No newline at end of file
diff --git a/gerberex/__init__.py b/gerberex/__init__.py
new file mode 100644
index 0000000..388f592
--- /dev/null
+++ b/gerberex/__init__.py
@@ -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
diff --git a/gerberex/am_expression.py b/gerberex/am_expression.py
new file mode 100644
index 0000000..7aa95c8
--- /dev/null
+++ b/gerberex/am_expression.py
@@ -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 = []
+
+
diff --git a/gerberex/am_primitive.py b/gerberex/am_primitive.py
new file mode 100644
index 0000000..3ce047a
--- /dev/null
+++ b/gerberex/am_primitive.py
@@ -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) \ No newline at end of file
diff --git a/gerberex/common.py b/gerberex/common.py
new file mode 100644
index 0000000..4a85bd4
--- /dev/null
+++ b/gerberex/common.py
@@ -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')
diff --git a/gerberex/composition.py b/gerberex/composition.py
new file mode 100644
index 0000000..2613a61
--- /dev/null
+++ b/gerberex/composition.py
@@ -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
diff --git a/gerberex/dxf.py b/gerberex/dxf.py
new file mode 100644
index 0000000..b641924
--- /dev/null
+++ b/gerberex/dxf.py
@@ -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)
diff --git a/gerberex/excellon.py b/gerberex/excellon.py
new file mode 100644
index 0000000..78e6e5f
--- /dev/null
+++ b/gerberex/excellon.py
@@ -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
diff --git a/gerberex/rs274x.py b/gerberex/rs274x.py
new file mode 100644
index 0000000..e9d82cd
--- /dev/null
+++ b/gerberex/rs274x.py
@@ -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)
diff --git a/gerberex/statements.py b/gerberex/statements.py
new file mode 100644
index 0000000..77ca235
--- /dev/null
+++ b/gerberex/statements.py
@@ -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()))
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..b9823a4
--- /dev/null
+++ b/setup.py
@@ -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()
diff --git a/test/panelimage.py b/test/panelimage.py
new file mode 100644
index 0000000..0636555
--- /dev/null
+++ b/test/panelimage.py
@@ -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')
diff --git a/test/test.py b/test/test.py
new file mode 100644
index 0000000..d894268
--- /dev/null
+++ b/test/test.py
@@ -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')