commit 68fd2318c4fa2c0345c2880e7ce366419dca0e79 Author: Richard George Date: Mon May 12 19:17:00 2014 +0100 Initial Revision DXF to Gerber converter diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9d6bd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,215 @@ +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +#packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg diff --git a/dxf_to_gerber.py b/dxf_to_gerber.py new file mode 100644 index 0000000..d994447 --- /dev/null +++ b/dxf_to_gerber.py @@ -0,0 +1,650 @@ +#!/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;