The KiCAD developers have kindly
created a nice set of technical drawing
templates that come bundled with their EDA software. The templates
are very well made, and even attempt to confrom to the ISO5457 standard
as much as possible. (This thread
on their developemnt is quite neat). I decided to translate to translate
the templates into a .svg
format that FreeCAD’s TechDraw
module could work with.
The results are fairly good:
If you want to use these templates yourself, processed copies (and the python scripts to generate them) are hosted on github. The rest of this articly goes through my process for writing the conversion scripts, in generally literate style.
First, we establish some constants to convert the original lisp-like template structure to svg. An appropriate parser and tokenizer script is borrowed from publicly available code. (Thanks to Mr. Derek Harter for making my life a little easier here!)
from lisp_like_parser import parse
from pprint import pprint
# A size paper dimensions in mm
= {
iso_pages "A2": [549,420],
"A3": [420,297],
"A4": [297,210],
"A4-portrait": [210,297]
}
# map KICAD's abbreviations for input fields to equivalent FreeCAD norms
= {
eq_editable "%C0": "Comment 1",
"%C1": "Comment 2",
"%C2": "Comment 3",
"%C3": "Comment 4",
"%S/%N": "SheetNo",
"%T": "Title",
"%Y": "Organization",
"%R": "Revision",
"%D": "Date",
}
We’ll use a simple state machine to loop through the different drawing elements and convert them to svg data (stored as strings). The start of each template file contains page layout and setup information:
def to_svg(ast):
# ast transformer to convert tokens to svg
= ""
result = ast[0]
cmd if cmd == "page_layout":
+= f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
result <!-- Generated with KiCAD2TechDraw: https://github.com/alexneufeld/KiCAD2TechDraw -->
<!-- Based on templates created by the KICAD developers: https://gitlab.com/kicad/libraries/kicad-templates -->
<svg
xmlns="http://www.w3.org/2000/svg" version="1.1"
xmlns:freecad="http://www.freecadweb.org/wiki/index.php?title=Svg_Namespace"
width="{PAGE_SIZE[0]}mm"
height="{PAGE_SIZE[1]}mm"
viewBox="0 0 {PAGE_SIZE[0]} {PAGE_SIZE[1]}">\n"""
for sub_ast in ast[1:]:
+= to_svg(sub_ast)
result += "</svg>\n"
result elif cmd == "setup":
global LINE_WIDTH
= ast[2][1]
LINE_WIDTH global LEFT_MARGIN
= ast[4][1]
LEFT_MARGIN global RIGHT_MARGIN
= ast[5][1]
RIGHT_MARGIN global TOP_MARGIN
= ast[6][1]
TOP_MARGIN global BOTTOM_MARGIN
= ast[7][1]
BOTTOM_MARGIN elif cmd == "line":
= parse_coord(ast[2])
x1, y1 = parse_coord(ast[3])
x2, y2 = ast[4][1]#*LINE_WIDTH
linewidth = ast[1][1]
ident # NOTE - 75% of spec'd linewidth seems to produce the most accurate results
+= f'<line id="{ident}" x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" style="stroke: black; stroke-width: {0.75*linewidth}pt; stroke-linecap: round; stroke-linejoin:round;"/>\n' result
Rectangles are used to layout title blocks and related drawing elements. These map directly to standard svg elements of the same name:
elif cmd == "rect":
= parse_coord(ast[2])
x1, y1 = parse_coord(ast[3])
x2, y2 if x2 > x1:
=x2-x1
width= x1
xs else:
=x1-x2
width=x2
xsif y2 > y1:
=y2-y1
height= y1
ys else:
=y1-y2
height= y2
ys = ast[4][1]#*LINE_WIDTH
linewidth = ast[1][1]
rect_name += f'<rect x="{xs}" y="{ys}" width="{width}" height="{height}" id="{rect_name}" style="stroke: black; stroke-width: {0.75*linewidth}pt; stroke-linecap: round; stroke-linejoin: round; fill: none;"/>\n' result
Converting text is a little more complicated. Adjustments need to be made so that sizing and position is as consistent as possible between fonts and formats. Many of the conversion constants here were figured out by trial-and-error.
elif cmd == "tbtext":
# need to handle either static or editable text
# quoted sentences also get split to multiple tokens
# It's all just a mess
= []
actual_text for subitem in ast[1:]:
if type(subitem) != list:
str(subitem))
actual_text.append(else:
break
= " ".join(actual_text).strip('"')
text_str = ast[len(actual_text)+1:]
rem #print(text_str)
= [0,0]
xpos, ypos = "left"
text_justify = 3.14159263
text_height = "No_ID"
text_id for item in rem:
if item[0] == "pos":
= parse_coord(item)
xpos,ypos elif item[0] == "justify":
= item[1]
text_justify elif item[0] == "font":
= item[2][2]
text_height elif item[0] == "name":
= item[1]
text_id if text_justify == "left":
= "start"
anchor else:
= "middle"
anchor # static text
if not text_str.startswith("%"):
# assign defaults
# NOTE: dy="{0.35*text_height}pt" compensates for differences between osifont and KiCAD's typical font geometry
+= f'<text x="{xpos}" y="{ypos}" transform="translate(0,{0.35*text_height})" id="{text_id}" style="font-size: {text_height}pt; text-anchor: {anchor}; fill: black; font-family: osifont">{text_str}</text>\n'
result else: # editable text
+= f'<text freecad:editable="{eq_editable[text_str]}" x="{xpos}" y="{ypos}" transform="translate(0,{0.35*text_height})" id="{text_id}" style="font-size: {text_height}pt; text-anchor: {anchor}; fill: black; font-family: osifont"><tspan>x</tspan></text>\n' result
The final element type that we care about is a filled polygon. These are used mainly for registration marks on the page corners.
elif cmd == "polygon":
= "none"
path_id = "0"
path_rotate = 0.35
path_line = []
thru_list = [0,0]
xp, yp for item in ast[1:]:
if item[0] == "name":
= item[1]
path_id elif item[0] == "rotate":
= 360-item[1]
path_rotate elif item[0] == "pos":
= parse_coord(item)
xp, yp elif item[0] == "linewidth":
= item[1]
path_line elif item[0] == "pts":
for pt in item[1:]:
1],pt[2]])
thru_list.append([pt[= ""
plist_str for xy in thru_list:
+= str(xy[0]) + "," + str(xy[1]) + " "
plist_str += f'<g transform="translate({xp},{yp})"><polygon id="{path_id}" transform="rotate({path_rotate})" points="{plist_str}" style="fill: solid black; stroke-width: {0.75*path_line}pt; stroke-linecap: round; stroke-linejoin: round;"/></g>\n'
result return result
We also define a coordinate parsing function. The original templates specified XY locations relative to any of the page corners. This convention makes converting to svg’s top left corner is the origin notation a little bit tedious, but it’s manageable.
def parse_coord(c):
# coordinates are specified relative to any one of the 4 page corners
# This is an 'interesting' design choice.
if len(c) == 4:
= c[3]
rel elif len(c) == 3:
= "rbcorner"
rel = c[1]
xi = c[2]
yi if rel == "ltcorner":
= xi+LEFT_MARGIN
x = yi+TOP_MARGIN
y elif rel == "lbcorner":
= xi+LEFT_MARGIN
x = -1*yi+PAGE_SIZE[1]-BOTTOM_MARGIN
y elif rel == "rtcorner":
= -1*xi+PAGE_SIZE[0]-RIGHT_MARGIN
x = yi+TOP_MARGIN
y elif rel == "rbcorner":
= PAGE_SIZE[0]-xi-RIGHT_MARGIN
x = -1*yi+PAGE_SIZE[1]-BOTTOM_MARGIN
y return [x,y]
And that’s it! We add in a main function to parse all of the templates we care about, and the script is ready to go.
if __name__ == "__main__":
for srcfile in os.listdir("kicad-templates/Worksheets"):
# only works with some of the templates for now
if not srcfile.startswith("A"):
continue
= srcfile.split("_")[0]
pagetype global PAGE_SIZE
= iso_pages[pagetype]
PAGE_SIZE # open the file and get the token list
= open(os.path.join("kicad-templates/Worksheets",srcfile),'r')
f = f.read()
contents = parse(contents)
x #pprint(x)
= to_svg(x)
svgstr = os.path.join("out",srcfile[:-10]+".svg")
outfile with open(outfile,'w') as g:
g.write(svgstr)print("Successfully exported to "+outfile)
Feel free to check out the full source code on github. Alternatively, download copies of the converted templates. Thanks for looking.