DXF to Kicad footprint conversion, python script (alpha)

Conversion of DXF polygons to Kicad Footprint with Python:

  1. Summary:
    a. Attached is a code to convert DXF rectangles, circles and isosceles right-angled triangles to Kicad footprints, of type rectangle, Trapezoidal (the triangles), circular, and circular through-hole vias.
    b. The designer should break the drawing to appropriate pats.
  2. Limitations:
    a. Only the following pad shapes are recognized: Rectangle (any angle), Right-angle isosceles triangle (trapezoid with maximum delta, any angle), Circle SMD, Circle Through-hole.
    b. DXF exported layers must be named with the following format:
    <pad #><SMD/TH>. Example: “F.Cu_1_SMD”
    c. DXF shapes must be of types LWPOLYLINE or CIRCLE.
    d. Through-hole vias are defined as two concentric circles, on a layer of type TH. Example:
    “F.Cu_2_TH”. This way the script identifies the outer circle as annular ring and inner circle as a hole.
    e. The code was tested against Keysight-ADS DXF exports. Other software output/results may differ.
  3. Code components:
    Note that this is early alpha version. Final implementation might be different.
    a. Dxf2fp.py contains two classes:
    i. “dxf2fpProcessClass”: should handle the conversion of DXF file to Kicad footprint. This includes setting header values (name, etc), handlind pad data and writing the file.
    ii. “dxf2fpPadClass”: Intermediate pad object, its methods perform conversion of the supported DXF entries to kicad required data.
    b. Currently, conversion process is performed by the test-bench file dxf_to_fp_V0.py.
    The finel implementation should include most operation within “dxf2fpProcessClass” methods.
  4. Example:
    a. Differential transmission line:
    ADS layout:

    Note how every potential pad number is defined in a different layer.
    Note the inner+outer circles of the through-hole layer.
    Note the connection of joint with circle pads to maintain connectivity in Kicad later.
    Export setting:

    Result from conversion to Kicad (running code with this exported DXF and unique name):

    3D view:
3 Likes
# -*- coding: utf-8 -*-
"""
Created on Sat Mar 26 21:57:25 2016
You may replicate, copy, paste, and publish this work, for either your
personal, buisness or educational purposes.
You must however credit me if you are using it in parts or as a whole.
As this is my private work, I bear no responsibility if it doesn't fit your
needs or messes up your work.
@author: Matan
"""
######################################################################
#dxf2fp_process Class - This is object handles the process of
#footprint conversion to an assortment of supported pads  
######################################################################

#Class constructor/properties:
###################################
class dxf2fpProcessClass(object):
    
    def __init__(self):
        #Set class properties (by pad definitions):
        self.fpName='not_set'
        self.fpDir='not_set'
        self.dimScale=1. #dimension scale for dxf scaling
        
        self.fpDescription='not_set'
        self.fpTags=[]
        
        self.fpRefName='not_set'
        self.fpRefLocXmm=0.
        self.fpRefLocYmm=0.
        self.fpRefLayer='not_set'
        self.fpRefSizeXmm=1.
        self.fpRefSizeYmm=1.
        self.fpRefThick=0.1
        self.fpRefHide=False
        
        self.fpValName='not_set'
        self.fpValLocXmm=0.
        self.fpValLocYmm=0.
        self.fpValLayer='F.Fab'
        self.fpValSizeXmm=1.
        self.fpValSizeYmm=1.
        self.fpValThick=0.1
        self.fpValHide=True
        
        self.abortReason='none'
        self.isAnnularProcessed=False
        
#Class methods:
###################################
    
    #Serial port open procedure:
    def identifyShape(self,shape):
        import numpy as np    
        import time
        from threading import Thread  
        self.serial1.baudrate=baud
        self.serial1.port=port       
######################################################################
#End dxf2fp_process Class
######################################################################

######################################################################
#Pad Class - This is an object class for the supported pad class types
######################################################################

#Class constructor/properties:
###################################
class dxf2fpPadClass(object):
    """Expected layer naming format:
    <layer name>_<pad_number>_<TH/SMD>
    Example:F.Cu_3_SMD
    Example2: dontCare_2_TH
    #Note: if it's a TH pad, the program expects 2 concentric cicle for hole/ring
    """
    def __init__(self):
        #Set class properties (by pad definitions):
        self.serNum=0 #serial number of pad
        self.raw='not_set' #raw polygon data
        self.type='not_set' #smd/through hole
        self.shape='not_set' #rectangle/trapezoindal(triangle included)/circle
        self.number=0 #pad nummber        
        self.copperLayer='not_set'
        self.longestSeg=0 #longestSeg=n => the longest line in the polygon is between points (n,n+1) ,circular with point 0
        self.locXmm=0
        self.locYmm=0
        self.angleDeg=0
        self.sizeXmm=0
        self.sizeYmm=0
        self.trapDmm=0 #long-short basis length difference (mm)
        self.holeDmm=0 #fore TH fp's this is the drill diameter
        self.paste='' #do we ass paste layer for this pad?
        self.mask='' #do we want to define mask OPENING for this pad?
        self.txt='' #text line corresponding to pad

#Class methods:
###################################
    #Serial port open procedure:
    def findShape(self,verbose):
        entitiy_shape=str(self.raw.dxftype) #raw data if of "entity" type
        if entitiy_shape=='CIRCLE': self.shape='circle' #identify circle
        else:
            if entitiy_shape=='LWPOLYLINE':
                #identifying trapezoidals/rectangles
                if len(self.raw.points)==3: self.shape='trapezoid'
                else:
                    if len(self.raw.points)==4: self.shape='rect'
                    else: self.shape='High_order_poly'
            else: self.shape='Unknows_type'
        if verbose: print('found ' +self.shape+ ' pad.')
    
    def findLongestSeg(self,verbose):
        import numpy as np
        if (self.shape!='circle'):
            dist_table=np.zeros(len(self.raw.points))
            for i in range(0,len(self.raw.points)):
                #calculation is done explicitly for clarity.
                x_vec=np.array([self.raw.points[i][0],self.raw.points[(i+1)%len(self.raw.points)][0]])
                y_vec=np.array([self.raw.points[i][1],self.raw.points[(i+1)%len(self.raw.points)][1]])
                dist_table[i]=(np.power(x_vec[1]-x_vec[0],2)+np.power(y_vec[1]-y_vec[0],2))
            self.longestSeg=dist_table.argmax(0)
            if verbose: print('longest segment is #'+str(self.longestSeg)+'.') 
        else:
            if verbose: print('No longest line for circle.')

    def setTypeNumLayer(self,verbose):
        #A function that set pad type (SMD/TH), pad # and pad Cu Layer
        #From pad name. WARNING: This function assumes those layer naming
        #is following the above defined layer naming convention.        
        self.copperLayer=str(self.raw.layer).split('_')[0]
        self.number=str(self.raw.layer).split('_')[1]
        self.type=str(self.raw.layer).split('_')[2]
        if self.type=='SMD': self.type='smd'
        else:
            if self.type=='TH': self.type='thru_hole'
            else: self.type=='unknown'
        if verbose: print('Set '+self.type+' Pad #'+ self.number+' @'
                            +self.copperLayer+'.')

    def setLocAndSizeMm(self,scale,verbose):
        import numpy as np
        #Set location of mid point in mm by dxf file and scale factor,
        #find rotation angle (in degrees) and calculate pad size 
        #with kicad comprehensible values:
        
        if (self.shape!='circle'):
            #Handle polygons:
            mod=len(self.raw.points) #set modulu divider
            i_l=self.longestSeg #define longest seg
            i_s=(i_l+2)%mod #define shortest seg circular
            x_mid_l=0.5*(self.raw.points[(i_l+1)%mod][0]+self.raw.points[i_l][0])
            y_mid_l=0.5*(self.raw.points[(i_l+1)%mod][1]+self.raw.points[i_l][1])
            dx_l=self.raw.points[(i_l+1)%mod][0]-self.raw.points[i_l][0]
            dy_l=self.raw.points[(i_l+1)%mod][1]-self.raw.points[i_l][1]
            if mod==4:
                #handle rectangles/trapz
                x_mid_s=0.5*(self.raw.points[(i_s+1)%mod][0]+self.raw.points[i_s][0])
                y_mid_s=0.5*(self.raw.points[(i_s+1)%mod][1]+self.raw.points[i_s][1])
                dx_s=self.raw.points[(i_s+1)%mod][0]-self.raw.points[i_s][0]
                dy_s=self.raw.points[(i_s+1)%mod][1]-self.raw.points[i_s][1]
            else:
                if mod==3:
                    #Handle triangles
                    x_mid_s=self.raw.points[i_s][0]
                    y_mid_s=self.raw.points[i_s][1]
                    dx_s=0.0
                    dy_s=0.0
                else:
                    #catch errors
                    x_mid_l=1.0e12
                    y_mid_l=1.0e12
                    x_mid_s=1.0e12
                    y_mid_s=1.0e12
            #set mid point location
            self.locXmm=scale*0.5*(x_mid_l+x_mid_s)
            self.locYmm=scale*0.5*(y_mid_l+y_mid_s)
            #set sizes:
            len_l=np.sqrt(np.power(dx_l,2)+np.power(dy_l,2))
            len_s=np.sqrt(np.power(dx_s,2)+np.power(dy_s,2))
            len_h=np.sqrt(np.power(x_mid_l-x_mid_s,2)+np.power(y_mid_l-y_mid_s,2))
            self.trapDmm=scale*(len_l-len_s)
            self.sizeXmm=scale*0.5*(len_l+len_s)
            self.sizeYmm=scale*len_h
            #Define rotation angle in degrees:
            norm_x=x_mid_s-x_mid_l
            norm_y=y_mid_s-y_mid_l
            self.angleDeg=90.+np.angle(norm_x+1j*norm_y,deg=1) #rotation relative to Y axis (that's how kicad uses it..)
        else:
            #Handle circles:
            self.locXmm=scale*self.raw.center[0]
            self.locYmm=scale*self.raw.center[1]
            self.sizeXmm=scale*2.*self.raw.radius
            self.sizeYmm=scale*2.*self.raw.radius
        if verbose:
            if ((self.locXmm==1.0e12) and (self.locXmm==1.0e12)):
                print('Center point calc error')
            else: print('center point: ('+str(np.round(self.locXmm,3))+','
                        +str(np.round(self.locYmm,3))+')mm, Rotation = '+
                        str(np.round(self.angleDeg,3))+' Degrees')
    
    def dat2txt(self):
        #This function translates pad data to kicad comprehensible text
        if self.shape=='trapezoid':
            self.txt=('pad '+str(self.number)+' '+self.type+' '+self.shape+
                      ' (at '+str(self.locXmm)+' '+str(-self.locYmm)+' '+str(self.angleDeg+180)+')'+
                      ' (size '+str(self.sizeXmm)+' '+str(self.sizeYmm)+') (rect_delta 0 '+str(self.trapDmm)+' )'+
                      '(layers '+self.copperLayer+' '+self.mask+' '+self.paste+')')
        if self.shape=='rect':
            self.txt=('pad '+str(self.number)+' '+self.type+' '+self.shape+
                      ' (at '+str(self.locXmm)+' '+str(-self.locYmm)+' '+str(self.angleDeg+180)+')'+
                      ' (size '+str(self.sizeXmm)+' '+str(self.sizeYmm)+' )'+
                      '(layers '+self.copperLayer+' '+self.mask+' '+self.paste+')')
        if self.shape=='circle':
            if self.type=='smd':
                self.txt=('pad '+str(self.number)+' '+self.type+' '+self.shape+
                          ' (at '+str(self.locXmm)+' '+str(-self.locYmm)+')'+
                          ' (size '+str(self.sizeXmm)+' '+str(self.sizeYmm)+' )'+
                          '(layers '+self.copperLayer+' '+self.mask+' '+self.paste+')')
            if self.type=='thru_hole':
                self.txt=('pad '+str(self.number)+' '+self.type+' '+self.shape+
                          ' (at '+str(self.locXmm)+' '+str(-self.locYmm)+')'+
                          ' (size '+str(self.sizeXmm)+' '+str(self.sizeYmm)+') (drill '+str(self.holeDmm)+' )'+
                          '(layers '+self.copperLayer+' '+self.mask+' '+self.paste+')')
        
######################################################################
#End Pad Class
######################################################################
    

1 Like
# -*- coding: utf-8 -*-
"""
Created on Sat Mar 26 21:08:50 2016
You may replicate, copy, paste, and publish this work, for either your
personal, buisness or educational purposes.
You must however credit me if you are using it in parts or as a whole.
As this is my private work, I bear no responsibility if it doesn't fit your
needs or messes up your work.
@author: Matan Gal-Katziri
"""
import numpy as np
import dxfgrabber
from dxf2pf_classes import dxf2fp_classes

"""Expected layer naming format:
<layer name>_<pad_number>_<TH/SMD>
Example:F.Cu_3_SMD
Example2: dontCF.Cuare_2_TH
#Note: if it's a TH pad, the program expects 2 concentric cicle for hole/ring
"""
#testcase import
dwg = dxfgrabber.readfile("J:\projects\Kicad_dxf_to_fp\Test_cases\dxf\\tline_tst.dxf")
#create lists of polygons per layer:
entity_list_by_layer=[]
for i in range(0,len(dwg.layers.names())):
    entity_list_by_layer.append([entity for entity in dwg.entities
                                if entity.layer == dwg.layers.names()[i]])

#This is the general dxf conversion routine, and should be eventually
#moved to the "dxf2fp_process class:
layer_list=[]
for layer in dwg.layers.names():
    layer_list.append(str(layer))
    print('Added layer '+layer+' to layer list')

#Create pads from entities of each layer:
#define master list of pad lists by layer names
list_pad_lists=[]
verbose=True
#create layer list variable (later to handled by dxf2fp_process object):

#process pads from dxf data:
n=0
for i in range(0,len(entity_list_by_layer)):
    pad_list=[]
    for j in range(0,len(entity_list_by_layer[i])):
        n+=1
        pad=dxf2fp_classes.dxf2fpPadClass()
        pad.serNum=n
        pad.raw=entity_list_by_layer[i][j]
        pad.findShape(verbose)
        pad.setTypeNumLayer(verbose)
        pad.findLongestSeg(verbose)
        pad.setLocAndSizeMm(0.001,verbose)
        pad_list.append(pad)
        
        #defineSize()
        #setProdLayers()
    list_pad_lists.append(pad_list)

#Process TH pads (Note! ring and hole must be defined in same layer)   
#process pads from dxf data:
sig_dig_err=3 #significant digits of allowed error for pin-hole misallignment
for i in range(0,len(list_pad_lists)):
    if verbose: print('Processing layer: "'+str(layer_list[i])+'"')
    if list_pad_lists[i]:  #ignore empty layers
        cells_to_remove=[]
        for j in range(0,len(list_pad_lists[i])): #for all pads in layer
            pad1=list_pad_lists[i][j]
            if pad1.type=='thru_hole': #identify "through hole"
                for k in range(j+1,len(list_pad_lists[i])): #look for another TH
                #In same layer with with same center coordinates.
                    pad2=list_pad_lists[i][k]
                    if ((pad2.type=='thru_hole') and 
                        (np.round(pad1.locXmm,sig_dig_err)==np.round(pad2.locXmm,sig_dig_err)) and
                        (np.round(pad1.locYmm,sig_dig_err)==np.round(pad2.locYmm,sig_dig_err))):
                        #define smaller TH as hole and lagrer as ring
                        if (pad1.sizeXmm>=pad2.sizeXmm):
                            pad1.holeDmm=pad2.sizeXmm
                            pad1.copperLayer='*.Cu'
                            list_pad_lists[i][j]=pad1
                        if (pad1.sizeXmm<pad2.sizeXmm):
                            pad_tmp=pad2
                            pad2=pad1
                            pad1=pad_tmp
                            pad1.holeDmm=pad2.sizeXmm
                            pad1.copperLayer='*.Cu'
                            list_pad_lists[i][j]=pad1
                        #print(k)
                        cells_to_remove.append(k)
                        if verbose: print("United pad #"+str(pad1.serNum)+" annular ring of "+str(pad1.sizeXmm)+"mm"
                                          " with pad #"+str(pad2.serNum)+" hole of "+str(pad1.holeDmm)+"mm")
        #create an updated pad list with no duplicates @ relevant layer
        updated_pad_list=[]
        #Populate pad list
        for j in range(0,len(list_pad_lists[i])):
            keep_flag=True
            for k in range(0,len(cells_to_remove)):
                if j==cells_to_remove[k]:
                    keep_flag=False
            if keep_flag:
                updated_pad_list.append(list_pad_lists[i][j])
                print ('keeping index '+ str(j)+ ' pad #'+str(list_pad_lists[i][j].serNum))
        #Replace original list with updated list
        list_pad_lists[i]=updated_pad_list

#Print job to footprint file:
make_fp=dxf2fp_classes.dxf2fpProcessClass()
#set process names&location:
make_fp.fpName='tlin_tst_example'
make_fp.fpDir='J:\\kicad2014\my_footprint2014\my_smd.pretty'
make_fp.fpDescription='Demo footprint import'
make_fp.fpTags='tline,RF,Planar,transmission'

make_fp.fpRefName='REF**'
make_fp.fpRefLocXmm=0.0
make_fp.fpRefLocYmm=0.0
make_fp.fpRefLayer='F.SilkS'
make_fp.fpRefSizeXmm=0.8
make_fp.fpRefSizeYmm=0.8
make_fp.fpRefThick=0.1
make_fp.fpRefHide=False

make_fp.fpValName='tline_diff'
make_fp.fpValLoc=0.0
make_fp.fpValLayer='F.Fab'
make_fp.fpValSizeXmm=0.8
make_fp.fpValSizeYmm=0.8
make_fp.fpValThick=0.1
make_fp.fpValHide=True

#open file
fp_file=open(make_fp.fpDir+'\\'+make_fp.fpName+'.kicad_mod','w')
#write header/value/reference
fp_file.write('(module '+make_fp.fpName+' (layer F.Cu)\n')
fp_file.write('  (fp_text reference '+make_fp.fpRefName+
              ' (at '+str(make_fp.fpRefLocXmm)+' '+str(make_fp.fpRefLocYmm)+
              ') (layer '+make_fp.fpRefLayer+')\n'+
              '    (effects (font (size '+str(make_fp.fpRefSizeXmm)+' '+str(make_fp.fpRefSizeYmm)+
              ') (thickness '+str(make_fp.fpRefThick)+')))\n'+
              '  )\n')
fp_file.write('  (fp_text value '+make_fp.fpValName+
              ' (at '+str(make_fp.fpValLocXmm)+' '+str(make_fp.fpValLocYmm)+
              ') (layer '+make_fp.fpValLayer+')\n'+
              '    (effects (font (size '+str(make_fp.fpValSizeXmm)+' '+str(make_fp.fpValSizeYmm)+
              ') (thickness '+str(make_fp.fpValThick)+')))\n'+
              '  )\n')
#Write pads to file
for i in range(0,len(list_pad_lists)):
    if verbose: print('Writing layer: "'+str(layer_list[i])+'"')
    if list_pad_lists[i]:  #ignore empty layers
        for j in range(0,len(list_pad_lists[i])): #for all pads in layer
            list_pad_lists[i][j].dat2txt()
            fp_file.write('  ('+list_pad_lists[i][j].txt+')\n')
#end fp_file
fp_file.write(')\n')
fp_file.close()
1 Like

For your information in Discourse you can display code like this:

def main():
    entity_list_by_layer=[]
    for i in range(0,len(dwg.layers.names())):
       entity_list_by_layer.append([entity for entity in dwg.entities
       if entity.layer == dwg.layers.names()[i]])

Just enclose it like so:

```python
def main():
    entity_list_by_layer=[]
    for i in range(0,len(dwg.layers.names())):
       entity_list_by_layer.append([entity for entity in dwg.entities
       if entity.layer == dwg.layers.names()[i]])
```
1 Like

Hi matangk,

thanks for posting this. This is exactly what I need. However, i am new to Kicad (but used a couple of EDA tools before, including microwave design suites). I am using Kicad-daily on Ubuntu-Mate 16.04, but did not use Kicad’s scripting before.

How would I use your scripts? Put them in ~.kicad_plugins ? Could you please be so kind to post a simple how-to what to do ? I am a bit confused with mapping the *.py files you mentioned to the code snippets (which one is which one,…)

I am eager to learn, but for now, I just need to get two microstrip filters I designed into footprints quickly.

Thanks & greetings

Hi,
I’m not using this script through the python plugins… Actually, I haven’t used Kicad python scripting yet myself.
I wrote and run it through my Anaconda python 2.7 installation, and use Spyder as a matlab-like console, as follows:

  1. Download and install your favorite python 2.7 IDE.
  2. Create your top directory and pick a name.
  3. In this directory, create a subdirectory: top-dir/dxf2pf_classes/
  4. The top file I posted is the pad class file. In your IDE, create a new python file, copy-paste the pad class to it, and save it under file top-dir/dxf2pf_classes/dxf2fp_classes.py
  5. In your IDE, create a new blank python file, delete all text if automatically added, and save it under top-dir/dxf2pf_classes/init.py
  6. The bottom file I posted is the example file. In your IDE, create a new python file, copy-paste the example file to it, and save it under file top-dir/filename.py
  7. In your IDE, define your working directory as top-dir/
  8. If you did everything correctly, you should now be able to import the pad class file to your console.
  9. To run the example you also need to install dxfgrabber and numpy packages.

To run the script all you need is the “class” and “example” files, and an input that you pre-prepared in your EM EDA suit (I wrote it for Keysight ADS so I can’t guarantee other software export similar DXF files…), according to the naming conventions written in the “example” file.

The output of the example file saves the footprint in a location of your choice.
After footprint creation I use kicad’s module editor with “open from a file” to import it.

I suggest you start with a simple toy footprint, and see how is goes.

BTW, One drawback I didn’t foresee when writing this is the inability to define inner layers SMD pads. I’m still looking for a good way of importing RF structures to multi-layer designs. However, since we usually only design RF sections on top layer, you can still use this script for top layer/ground-vias, and manually add high-priority ground plane underneath in the PCB file.

Best
M

I think KiCAD dislikes internal SMD pads… last time I tried that with some older nightly build PCBnew had a hissy fit about it. YMMV though.

Hi,
Sorry, I’m not a native English speaker - so either I don’t understand what you wrote, or I don’t understand what I wrote, or we’re saying the same thing :slight_smile:

Re-reading your’s I think we’re saying the same thing, but I misunderstood you earlier. :blush:

I’d expect pure SMD internals to cause KiCad to complain, as it needs a drill hole in order to make sense to have nets on inner layers.

However, there is a bug in KiCad if you have a complex Pad Stack, that has SMD + Drill on F.Cu and then a SMD on B.Cu of differing geometry.
In this case, it spits false errors of Drill near PAD, and also fails thermal generation.
Creating two identical drill stacks, is a bad idea, but that does remove one error message.
ie … some work is needed in the details of ‘own pad number drill’ handling.

1 Like

Hi matangk,

thanks a lot, this helps !

Best regards,
L.