⇦ Return to Homepage

⇦ Return to Post Listing

Converting KiCAD drawing templates for use in FreeCAD

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 = ""
    cmd = ast[0]
    if cmd == "page_layout":
        result += f"""<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- 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:]:
            result += to_svg(sub_ast)
        result += "</svg>\n"
    elif cmd == "setup":
        global LINE_WIDTH
        LINE_WIDTH = ast[2][1]
        global LEFT_MARGIN
        LEFT_MARGIN = ast[4][1]
        global RIGHT_MARGIN
        RIGHT_MARGIN = ast[5][1]
        global TOP_MARGIN
        TOP_MARGIN = ast[6][1]
        global BOTTOM_MARGIN
        BOTTOM_MARGIN = ast[7][1]
    elif cmd == "line":
        x1, y1 = parse_coord(ast[2])
        x2, y2 = parse_coord(ast[3])
        linewidth = ast[4][1]#*LINE_WIDTH
        ident = ast[1][1]
        # NOTE - 75% of spec'd linewidth seems to produce the most accurate results
        result += 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'

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":
        x1, y1 = parse_coord(ast[2])
        x2, y2 = parse_coord(ast[3])
        if x2 > x1:
            width=x2-x1
            xs = x1
        else:
            width=x1-x2
            xs=x2
        if y2 > y1:
            height=y2-y1
            ys = y1
        else:
            height=y1-y2
            ys = y2
        linewidth = ast[4][1]#*LINE_WIDTH
        rect_name = ast[1][1]
        result += 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'

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:
                actual_text.append(str(subitem))
            else:
                break
        text_str = " ".join(actual_text).strip('"')
        rem = ast[len(actual_text)+1:]
        #print(text_str)
        xpos, ypos = [0,0]
        text_justify = "left"
        text_height = 3.14159263
        text_id = "No_ID"
        for item in rem:
            if item[0] == "pos":
                xpos,ypos = parse_coord(item)
            elif item[0] == "justify":
                text_justify = item[1]
            elif item[0] == "font":
                text_height = item[2][2]
            elif item[0] == "name":
                text_id = item[1]
        if text_justify == "left":
            anchor = "start"
        else:
            anchor = "middle"
        # 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
            result += 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'
        else: # editable text
            result += 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'

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":
        path_id = "none"
        path_rotate = "0"
        path_line = 0.35
        thru_list = []
        xp, yp = [0,0]
        for item in ast[1:]:
            if item[0] == "name":
                path_id = item[1]
            elif item[0] == "rotate":
                path_rotate = 360-item[1]
            elif item[0] == "pos":
                xp, yp = parse_coord(item)
            elif item[0] == "linewidth":
                path_line = item[1]
            elif item[0] == "pts":
                for pt in item[1:]:
                    thru_list.append([pt[1],pt[2]])
            plist_str = ""
            for xy in thru_list:
                plist_str += str(xy[0]) + "," + str(xy[1]) + " "
        result += 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'
    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:
        rel = c[3]
    elif len(c) == 3:
        rel = "rbcorner"
    xi = c[1]
    yi = c[2]
    if rel == "ltcorner":
        x = xi+LEFT_MARGIN
        y = yi+TOP_MARGIN
    elif rel == "lbcorner":
        x = xi+LEFT_MARGIN
        y = -1*yi+PAGE_SIZE[1]-BOTTOM_MARGIN
    elif rel == "rtcorner":
        x = -1*xi+PAGE_SIZE[0]-RIGHT_MARGIN
        y = yi+TOP_MARGIN
    elif rel == "rbcorner":
        x = PAGE_SIZE[0]-xi-RIGHT_MARGIN
        y = -1*yi+PAGE_SIZE[1]-BOTTOM_MARGIN
    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
        pagetype = srcfile.split("_")[0]
        global PAGE_SIZE
        PAGE_SIZE = iso_pages[pagetype]
        # open the file and get the token list
        f = open(os.path.join("kicad-templates/Worksheets",srcfile),'r')
        contents = f.read()
        x = parse(contents)
        #pprint(x)
        svgstr = to_svg(x)
        outfile = os.path.join("out",srcfile[:-10]+".svg")
        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.