Files
DXFtoGerber/dxf_to_gerber.py
Richard George 68fd2318c4 Initial Revision
DXF to Gerber converter
2014-05-12 19:17:00 +01:00

651 lines
21 KiB
Python

#!/usr/bin/env python
# Convert a DXF file to a series of Gerber and Excellon files suitable for e.g. PCB Train
#
# Layer names on the DXF file are used to determine how the objects in the DXF file map onto the circuit board
#
# A two-layer PCB can be represented as follows:
#
# (G) Top Overlay -> .gto
# (G) Top Soldermask -> .gts
# (G) Top Copper -> .gtl
# (E) Drill -> .gdd
# (G) Bottom Copper -> .gbl
# (G) Bottom Overlay -> .gbo
# (G) Bottom Soldermask -> .gbs
# (E) Mechanical -> .gm1
#
# where 'E' will become an Excellon file (CNC drill or routing), and 'G' will become a Gerber file (PCB exposure)
#
# The converter knows how to handle two types of objects, polylines and circles
#
# For Gerber files
#
# Closed polylines -> translate into filled areas of copper, open solder masks, and filled overlay
# Open polylines -> tracks on the copper layer, lines on the overlay/silkscreen
# Circles -> circular aperture flashes on copper, silkscreen and soldermask layers
#
# For Excellon files
#
# Circles -> drilled holes on 'Drill' layer
# Open Polylines -> Cut-out slots on Mechanical layer
# Closed Polylines -> Cut-out pieces, or boundary of whole PCB
#
# Open polylines need the "Global Linewidth" property set in AutoCAD to define how wide
#
# Deficiencies / to be implemented:
#
# Could process drills sensibly: we currently output tool codes for unused holes
# Cut-outs are not implemented on the mechanical layer
#
import math;
import re;
import os;
import glob;
class DXFFile:
X = 10;
Y = 20;
Z = 30;
DIAMETER = 40;
LINEWIDTH = 41;
POLYLINE_FLAGS = 70;
BULGE = 42;
LAYER = 8;
POLYLINE_FLAG_CLOSED = 1;
prec = 8;
def __init__(self,fname):
self.polylines = list();
self.circles = list();
self.layers = set();
self.filename = fname;
# An entry in a DXF file consists of
# integer
# text
#
# e.g.
# 10
# x-coordinate
# 20
# y-coordinate
#
# We build a dictionary that allows look-up of a parser for the next line of input, based on the integer code
#
rev_parse = { (lambda a: round(float(a)*self.prec)/self.prec):(self.X,self.Y,self.Z,self.LINEWIDTH,self.BULGE,self.DIAMETER), (lambda a: str(a)):(self.LAYER,), (lambda a: int(a)):(self.POLYLINE_FLAGS,) };
self.parse = dict();
for k in rev_parse:
for j in rev_parse[k]:
self.parse[j]=k;
with open(fname) as f:
self.read_dxf_file(f);
def read_entity(self,f):
results = {};
while True:
try:
t = f.readline().strip();
l1=int(t);
except ValueError:
try:
l1=int(t,16);
except ValueError:
l1 = f.readline();
continue;
if l1==0:
break;
if l1 in self.parse:
l2=f.readline().strip();
results[l1]=self.parse[l1](l2);
return results;
def read_circle(self,f):
c = self.read_entity(f);
# return diameter, not radius
if 40 in c:
c[40] = c[40] * 2.0;
self.circles.append(c);
def read_polyline(self,f):
result = list();
this_entity = self.read_entity(f);
while True:
l = f.readline().strip();
if l=='SEQEND':
break;
if l=='VERTEX':
result.append(self.read_entity(f));
this_entity['VERTICES'] = result;
self.polylines.append(this_entity);
def read_dxf_file(self,f):
while True:
l=f.readline().strip();
if l=='EOF':
break;
if l=='CIRCLE':
self.read_circle(f);
if l=='POLYLINE':
self.read_polyline(f);
@staticmethod
def matches(a,b):
return re.sub(' ','_',a.strip().lower())==re.sub(' ','_',b.strip().lower());
def circles_on_layer(self,n):
for c in self.circles:
if self.matches(c[self.LAYER],n):
yield c;
def polylines_on_layer(self,n):
for p in self.polylines:
if self.matches(p[self.LAYER],n):
yield p;
def open_polylines_on_layer(self,n):
for p in self.polylines:
if self.matches(p[self.LAYER],n):
if self.POLYLINE_FLAGS not in p:
yield p;
else:
if (p[self.POLYLINE_FLAGS] & self.POLYLINE_FLAG_CLOSED)==0:
yield p;
def closed_polylines_on_layer(self,n):
for p in self.polylines:
if self.matches(p[self.LAYER],n):
if self.POLYLINE_FLAGS in p:
if (p[self.POLYLINE_FLAGS] & self.POLYLINE_FLAG_CLOSED)!=0:
yield p;
def diameters(self,circles,layer='ALL'):
result = set();
for c in circles:
if (self.matches(c[self.LAYER],layer)) | (layer=='ALL'):
result.add(c[self.DIAMETER]);
return result;
def linewidths(self,polylines,layer='ALL'):
result = set();
for p in polylines:
if (self.matches(p[self.LAYER],layer)) | layer=='ALL':
result.add(p[self.LINEWIDTH]);
return result;
def layer_names(self):
result = set();
for c in self.circles:
result.add(c[self.LAYER]);
for p in self.polylines:
result.add(p[self.LAYER]);
return result;
class GerberWriter:
precision = (2,6);
scale = 1.0;
default_diameter = 0.01;
gerber_layers = { \
'.gbl':('Bottom Copper','Bottom'), \
'.gbo':('Bottom Outlines','Bottom Overlay'), \
'.gbs':('Bottom Soldermask',), \
'.gtl':('Top Copper','Top',), \
'.gto':('Top Outlines','Top Overlay'), \
'.gts':('Top Soldermask',)};
mechanical_layers = {'.gm1':('Mechanical','Cutout','Cut Out')};
excellon_layers = { \
'.gdd':('Drill',) };
@staticmethod
def emit_coord(d):
s = d * GerberWriter.scale;
result = ('%%%dd' % (GerberWriter.precision[0]+GerberWriter.precision[1])) % int(s*pow(10.0,GerberWriter.precision[1]));
return result.strip();
@staticmethod
def exc_emit_coord(d):
s = d * GerberWriter.scale;
return re.sub('^0+','','%06.2f' % (s));
@staticmethod
def exc_emit_point(p):
result ='X%s' % GerberWriter.exc_emit_coord(p[0]);
result +='Y%s' % GerberWriter.exc_emit_coord(p[1]);
return result;
def flash_command(self,f,p,c):
self.emit_command(f,self.emit_point(p)+c);
@staticmethod
def emit_command(f,symbol,value=""):
if symbol=='G04':
print >> f, "G04 %s*" % (value.strip());
else:
print >> f, "%s%s*" % (symbol,value);
@staticmethod
def emit_parameter(f,p,value):
print >> f,"%%%s%s*%%" % (p,value);
@staticmethod
def emit_precision(f):
GerberWriter.emit_parameter(f,"FS","LAX%d%d" % (GerberWriter.precision[0],GerberWriter.precision[1]));
@staticmethod
def XthenY(a,b):
if a[DXFFile.X] < b[DXFFile.X]:
return -1;
else:
if a[DXFFile.X] > b[DXFFile.X]:
return 1;
else:
if a[DXFFile.Y] < b[DXFFile.Y]:
return -1;
else:
if a[DXFFile.Y] > b[DXFFile.Y]:
return 1;
else:
return a[DXFFile.DIAMETER] < b[DXFFile.DIAMETER];
@staticmethod
def no_duplicates(k):
First = True;
for j in k:
if First:
i = j;
yield i;
First = False;
else:
if GerberWriter.XthenY(i,j)==0:
continue;
else:
i = j;
yield i;
def emit_point(self,p):
result='';
if self.X!=p[0]:
result +='X%s' % GerberWriter.emit_coord(p[0]);
self.X=p[0];
if self.Y!=p[1]:
result +='Y%s' % GerberWriter.emit_coord(p[1]);
self.Y=p[1];
return result;
def draw_to(self,f,p):
self.emit_command(f,self.emit_point(p)+"D01");
def move_to(self,f,p):
self.emit_command(f,self.emit_point(p)+"D02");
def emit_level(self,f,dark=True):
if dark:
self.emit_parameter(f,"LP","D");
self.level_dark = True;
else:
self.emit_parameter(f,"LP","C");
self.level_dark = False;
def clear_aperture_cache(self):
self.circular_apertures = set();
self.aperture_codes = dict();
self.aperture_diameters = dict();
self.excellon_drill_diameters = dict();
self.excellon_drill_codes = dict();
self.current_aperture_code = -1;
self.excellon_drill_counter = -1;
self.current_excellon_drill_code = -1;
## INIT METHOD
def __init__(self):
self.clear_aperture_cache();
'''Measure the DXF file, record circular apertures'''
def measure_dxf_file(self,dxf):
for c in dxf.circles:
self.circular_apertures.add(c[DXFFile.DIAMETER]);
for p in dxf.polylines:
if DXFFile.LINEWIDTH in p:
self.circular_apertures.add(p[DXFFile.LINEWIDTH]);
else:
self.circular_apertures.add(0.0);
def process_dxf_for_writing(self,dxf,layernames):
regions = list();
tracks = list();
circles = list();
# Process tracks
for layer in layernames:
for p in dxf.open_polylines_on_layer(layer):
tracks.append(p);
# Process regions
for layer in layernames:
for p in dxf.closed_polylines_on_layer(layer):
regions.append(p);
# Process circles
for layer in layernames:
for p in dxf.circles_on_layer(layer):
circles.append(p);
return {'Tracks':tracks,'Regions':regions,'Circles':circles};
def emit_gerber_aperture_definition(self,f,n,s):
self.emit_parameter(f,"ADD%d" % n,s);
def define_gerber_circular_aperture(self,f,n,d):
dia = self.default_diameter if d==0.0 else d;
self.emit_gerber_aperture_definition(f,n,"C,%f" % (dia*self.scale));
self.aperture_diameters[d]=n;
self.aperture_codes[n]=d;
def ensure_region(self,f,state=True):
if self.region!=state:
if state:
self.emit_command(f,"G36");
self.region=True;
else:
self.emit_command(f,"G37");
self.region=False;
def emit_region(self,f,poly):
self.ensure_region(f,True);
points = iter(poly['VERTICIES']);
first_point = points.next();
self.move_to(f,first_point);
for point in points:
self.draw_to(f,point);
self.draw_to(f,first_point);
def write_gerber_select_aperture(self,f,c):
req_aperture_code = self.aperture_diameters[c];
if self.current_aperture_code!=req_aperture_code:
self.emit_command(f,'D%d' % req_aperture_code);
self.current_aperture_code = req_aperture_code;
def reset_gerber_state(self,f):
self.emit_level(f,dark=True);
self.region=False;
self.X = -1.0;
self.Y = -1.0;
self.current_aperture_diameter = -1;
self.current_aperture_code = -1;
self.current_drill_diameter = -1;
def write_gerber_header(self,f):
self.emit_parameter(f,"G04","Lancaster University RF PCB");
self.emit_precision(f);
self.emit_parameter(f,"MO","MM");
self.emit_parameter(f,"SR","X1Y1I0J0");
self.reset_gerber_state(f);
def write_gerber_apertures(self,f):
self.aperture_counter = 10;
for c in self.circular_apertures:
self.define_gerber_circular_aperture(f,self.aperture_counter,c);
self.aperture_counter += 1;
def write_gerber_track(self,f,poly):
self.ensure_region(f,False);
if DXFFile.LINEWIDTH in poly:
self.write_gerber_select_aperture(f,poly[DXFFile.LINEWIDTH]);
else:
self.write_gerber_select_aperture(f,0.0);
print "Bug! writing a zero-width open line";
points = iter(poly['VERTICES']);
first_point = points.next();
self.move_to(f,(first_point[DXFFile.X],first_point[DXFFile.Y]));
for point in points:
self.draw_to(f,(point[DXFFile.X],point[DXFFile.Y]));
def write_gerber_region(self,f,poly):
self.ensure_region(f,True);
points = iter(poly['VERTICES']);
first_point = points.next();
self.move_to(f,(first_point[DXFFile.X],first_point[DXFFile.Y]));
for point in points:
self.draw_to(f,(point[DXFFile.X],point[DXFFile.Y]));
self.draw_to(f,(first_point[DXFFile.X],first_point[DXFFile.Y]));
def write_gerber_flash(self,f,c):
if c[DXFFile.X]==0.0:
if c[DXFFile.Y]==0.0:
return
self.write_gerber_select_aperture(f,c[DXFFile.DIAMETER]);
self.flash_command(f,(c[DXFFile.X],c[DXFFile.Y]),'D03');
def write_gerber_trailer(self,f):
self.ensure_region(f,False);
self.emit_command(f,"M02");
def write_gerber_file(self,fname,dxf,layernames):
print 'Writing Gerber file %s' % fname;
entities = self.process_dxf_for_writing(dxf,layernames);
print 'File will contain %d regions, %d tracks and %d circles' % (len(entities['Regions']),len(entities['Tracks']),len(entities['Circles']));
if len(entities['Regions'])==0:
if len(entities['Tracks'])==0:
if len(entities['Circles'])==0:
print "File will be empty: Skipping file %s" % fname;
try:
os.unlink(fname);
except:
pass;
return
with open(fname,'w') as f:
self.write_gerber_header(f);
self.write_gerber_apertures(f);
print "Writing %d Tracks" % (len(entities['Tracks']));
for c in self.circular_apertures:
for p in entities['Tracks']:
if DXFFile.LINEWIDTH in p:
if p[DXFFile.LINEWIDTH]==c:
self.write_gerber_track(f,p);
else:
if c==0.0:
self.write_gerber_track(f,p);
print "Flashing %d Apertures" % (len(self.circular_apertures));
for d in self.circular_apertures:
for c in self.no_duplicates(sorted(list(entities['Circles']),cmp=self.XthenY)):
if d==c[DXFFile.DIAMETER]:
self.write_gerber_flash(f,c);
print "Writing %d Regions" % (len(entities['Regions']));
for r in entities['Regions']:
self.write_gerber_region(f,r);
self.write_gerber_trailer(f);
# To do with excellon
def write_excellon_header(self,f):
print >> f, "%";
print >> f, "M48";
print >> f, "METRIC,TZ";
print >> f, "M71";
def define_excellon_drill_diameter(self,f,n,d):
dia = self.default_diameter if d==0.0 else d;
print >> f, "T%02dC%4.3f" % (n,math.ceil(dia*10.0)/10.0);
self.excellon_drill_diameters[d]=n;
self.excellon_drill_codes[n]=d;
def write_excellon_drills(self,f):
self.excellon_drill_counter = 1;
for c in self.circular_apertures:
if c==0.0:
continue;
self.define_excellon_drill_diameter(f,self.excellon_drill_counter,c);
self.excellon_drill_counter += 1;
def write_excellon_select_drill(self,f,d):
req_drill_code = self.excellon_drill_diameters[d];
if self.current_excellon_drill_code!=req_drill_code:
print >> f, "T%02d" % req_drill_code;
self.current_excellon_drill_code = req_drill_code;
def write_excellon_cut(self,f,p):
pass;
def write_excellon_cutout(self,f,p):
pass;
def write_excellon_drill_point(self,f,c):
self.write_excellon_select_drill(f,c[DXFFile.DIAMETER]);
print >> f, GerberWriter.exc_emit_point((c[DXFFile.X],c[DXFFile.Y]));
def write_excellon_trailer(self,f):
print >> f, "M30";
def write_excellon_file(self,fname,dxf,layernames):
print 'Writing Excellon file %s' % fname;
killfile = True;
entities = self.process_dxf_for_writing(dxf,layernames);
diameters = sorted(list(self.circular_apertures));
with open(fname,'w') as f:
self.write_excellon_header(f);
self.write_excellon_drills(f);
print >> f, "%";
print >> f, "G05";
for dia in diameters:
print "Diameter = %g" % (dia);
if dia==0.0:
print "Skipping diameter 0 holes";
continue;
print "Processing entries for drill diameter %g" % (dia);
holes = list(self.no_duplicates(sorted(entities['Circles'],cmp=self.XthenY)));
print "Drilling %d holes\n" % len(holes);
for circle in holes:
if circle[DXFFile.DIAMETER]==dia:
self.write_excellon_drill_point(f,circle);
# print "Making %d cuts\n" % len(entities['Tracks']);
#
# print >> f, "G01";
#
# for p in entities['Tracks']:
# if DXFFile.LINEWIDTH in p:
# if p[DXFFile.LINEWIDTH]==dia:
# self.write_excellon_cut(f,p);
# else:
# raise Exception("Error: trying to cut a slot with zero cutter width");
#
# print "Making %d cut-outs\n" % (len(entities['Regions']));
#
# print >> f, "G01";
#
# for r in entities['Regions']:
# print r;
# if DXFFile.LINEWIDTH in r:
# print "Cut-out has width %g" % (r[DXFFile.LINEWIDTH]);
# if r[DXFFile.LINEWIDTH]==dia:
# self.write_excellon_cutout(f,r);
# else:
# raise Exception("Error: trying to cut a cut-out with zero cutter width");
self.write_excellon_trailer(f);
def process_cam(self,dxf,camname=None):
self.clear_aperture_cache();
self.measure_dxf_file(dxf);
# Pick a sensible output filename
if camname == None:
camname = dxf.filename;
self.cam_base = os.path.splitext(camname)[0];
# For each layer, produce a Gerber or Excellon file
print "\n\nProcessing Gerber files\n";
for extension in self.gerber_layers:
ofname = self.cam_base+extension;
print "Writing data of type %s to file %s" % (self.gerber_layers[extension][0],ofname);
self.write_gerber_file(ofname,dxf,self.gerber_layers[extension]);
print "";
print "\n\nProcessing Excellon files\n";
# For each layer, produce a Gerber or Excellon file
for extension in self.excellon_layers:
ofname = self.cam_base+extension;
print "Writing data of type %s to file %s" % (self.excellon_layers[extension][0],ofname);
self.write_excellon_file(ofname,dxf,self.excellon_layers[extension]);
print "";
print "\n\nDone\n";
# The main program
if __name__=="__main__":
for f in glob.glob('G:\\resonator_board\\*.dxf'):
print 'Processing file %s' % f;
d = DXFFile(f);
g = GerberWriter();
g.process_cam(d);
del g;
del d;