Generating a PCB ruler (cm and inches) with a pcbnew Python script

Published on October 21, 2026 by leZ

A real 15 cm / 5 inch PCB ruler drawn by a pcbnew Python script: gold copper ticks, silkscreen numbers, SMD size and drill gauges, 0 DRC errors.

15 cm / 5 inch PCB ruler generated by a pcbnew Python script: gold copper ticks on black solder mask, SMD size and drill gauges
KiCad's 3D render: black solder mask, gold copper, 1:1 scale.

A PCB ruler is the classic giveaway from board manufacturers, and it makes a great scripting exercise: 288 objects to place to a tenth of a millimetre, impossible to draw cleanly by hand, yet they fit in a few Python loops. This project produces a real, manufacturable ruler: 15 cm on one edge, 5 inches on the other, an SMD package size gauge and a drill gauge, with 0 DRC errors.

The whole build as a step-by-step video (6:24, with subtitles):

It all relies on three building blocks already covered in the tutorials: segments (PCB_SHAPE), silkscreen text (PCB_TEXT) and holes drawn on Edge.Cuts.

The idea: one loop and a modulo

Each tick is a vertical segment. Its length depends only on its position: a long tick every centimetre, a medium one every 5 mm, a short one otherwise. The modulo % does all the work:

for i in range(151):                                   # one tick per mm, 0 to 15 cm
    ln = 4.5 if i % 10 == 0 else 3.0 if i % 5 == 0 else 1.8
    seg(board, (ZERO + i, Y0 + MARGIN), (ZERO + i, Y0 + MARGIN + ln))

The inch scale follows the same logic with a 25.4 / 16 mm step and five tick lengths (inch, 1/2, 1/4, 1/8, 1/16).

Change the length below to see how many objects the script creates:

for i in range(151):        # one tick per mm
    ln = 4.5 if i % 10 == 0 else 3.0 if i % 5 == 0 else 1.8
    seg(board, (ZERO + i, Y0 + MARGIN), (ZERO + i, Y0 + MARGIN + ln))
15
0 ticks and numbers · board 162 × 32 mm

The complete script

The script creates an empty board, draws everything, then saves pcb_ruler.kicad_pcb. Run it with the Python bundled with KiCad (see the setup guide), for example from the KiCad Command Prompt: python pcb_ruler.py.

import pcbnew
from pcbnew import Edge_Cuts, F_Cu, F_Mask, F_SilkS, FromMM, PCB_SHAPE, PCB_TEXT, VECTOR2I

X0, Y0, X1, Y1 = 100.0, 100.0, 262.0, 132.0   # board outline, mm
ZERO = 105.0       # x of the 0 mark, shared by both scales
MARGIN = 0.75      # copper-to-edge clearance (DRC wants 0.5 mm + half the tick width)
TICK_W = 0.22


def mm(x, y):
    return VECTOR2I(FromMM(x), FromMM(y))


def shape(board, kind, layer, width):
    s = PCB_SHAPE(board)
    s.SetShape(kind)
    s.SetLayer(layer)
    s.SetWidth(FromMM(width))
    board.Add(s)
    return s


def seg(board, a, b, layer=F_Cu, w=TICK_W):
    s = shape(board, pcbnew.SHAPE_T_SEGMENT, layer, w)
    s.SetStart(mm(*a))
    s.SetEnd(mm(*b))
    return s


def circle(board, c, r, layer=Edge_Cuts, w=0.1):
    s = shape(board, pcbnew.SHAPE_T_CIRCLE, layer, w)
    s.SetCenter(mm(*c))
    s.SetEnd(mm(c[0] + r, c[1]))
    return s


def rect(board, cx, cy, w, h):
    s = shape(board, pcbnew.SHAPE_T_RECTANGLE, F_Cu, 0.05)
    s.SetStart(mm(cx - w / 2, cy - h / 2))
    s.SetEnd(mm(cx + w / 2, cy + h / 2))
    s.SetFilled(True)
    return s


def text(board, s, x, y, size=1.6, bold=False):
    t = PCB_TEXT(board)
    t.SetText(s)
    t.SetPosition(mm(x, y))
    t.SetLayer(F_SilkS)
    t.SetTextSize(VECTOR2I(FromMM(size), FromMM(size)))
    t.SetTextThickness(FromMM(size * (0.2 if bold else 0.15)))
    board.Add(t)


def outline(board, r=3.0):
    for a, b in (((X0 + r, Y0), (X1 - r, Y0)), ((X1, Y0 + r), (X1, Y1 - r)),
                 ((X1 - r, Y1), (X0 + r, Y1)), ((X0, Y1 - r), (X0, Y0 + r))):
        seg(board, a, b, Edge_Cuts, 0.1)
    for c, start in (((X0 + r, Y0 + r), (X0, Y0 + r)), ((X1 - r, Y0 + r), (X1 - r, Y0)),
                     ((X1 - r, Y1 - r), (X1, Y1 - r)), ((X0 + r, Y1 - r), (X0 + r, Y1))):
        a = shape(board, pcbnew.SHAPE_T_ARC, Edge_Cuts, 0.1)
        a.SetCenter(mm(*c))
        a.SetStart(mm(*start))
        a.SetArcAngleAndEnd(pcbnew.EDA_ANGLE(90, pcbnew.DEGREES_T), True)
    circle(board, (X1 - 6, (Y0 + Y1) / 2), 2.5)        # hanging hole


def metric_scale(board):
    for i in range(151):                                   # one tick per mm, 0 to 15 cm
        ln = 4.5 if i % 10 == 0 else 3.0 if i % 5 == 0 else 1.8
        seg(board, (ZERO + i, Y0 + MARGIN), (ZERO + i, Y0 + MARGIN + ln))
    for c in range(16):
        text(board, str(c), ZERO + 10 * c, Y0 + 6.9, 1.7)
    text(board, "cm", ZERO + 154, Y0 + 6.9, 1.4)


def inch_scale(board):
    for i in range(5 * 16 + 1):                            # 1/16 inch ticks, 0 to 5 in
        ln = 4.5 if i % 16 == 0 else 3.2 if i % 8 == 0 else 2.4 if i % 4 == 0 else 1.7 if i % 2 == 0 else 1.1
        x = ZERO + i * 25.4 / 16
        seg(board, (x, Y1 - MARGIN), (x, Y1 - MARGIN - ln))
    for n in range(6):
        text(board, str(n), ZERO + 25.4 * n, Y1 - 6.9, 1.7)
    text(board, "inch", ZERO + 25.4 * 5 + 6, Y1 - 6.9, 1.4)


def gauges(board, ym=(Y0 + Y1) / 2):
    x = 108.0
    for name, w, h in [("0201", 0.6, 0.3), ("0402", 1.0, 0.5), ("0603", 1.6, 0.8),
                       ("0805", 2.0, 1.25), ("1206", 3.2, 1.6), ("2512", 6.3, 3.2)]:
        rect(board, x + w / 2, ym - 0.6, w, h)             # actual SMD body size
        text(board, name, x + w / 2, ym + 2.4, 1.0)
        x += max(w, 4.0) + 2.6
    x = 166.0
    for d in [0.5, 0.8, 1.0, 1.5, 2.0, 3.0]:
        c = (x + d / 2, ym - 0.6)
        circle(board, c, d / 2)                            # real hole (Edge.Cuts)
        circle(board, c, d / 2 + 0.85, F_Cu, 0.6)          # copper ring, 0.55 mm from the hole
        text(board, f"{d:g}", c[0], max(ym + 2.4, c[1] + d / 2 + 2.35), 1.0)
        x += d + 4.2
    text(board, "ScriptedPCB", 228.0, ym - 1.2, 2.2, bold=True)
    text(board, "drawn with Python", 228.0, ym + 1.9, 1.1)


def expose_copper(board):
    """Same shapes on F.Mask: no solder mask over the copper, so it comes out gold (ENIG)."""
    for s in list(board.GetDrawings()):
        if isinstance(s, PCB_SHAPE) and s.GetLayer() == F_Cu:
            m = s.Duplicate()
            m.SetLayer(F_Mask)
            m.SetWidth(s.GetWidth() + FromMM(0.08))
            board.Add(m)


board = pcbnew.CreateEmptyBoard()
outline(board)
metric_scale(board)
inch_scale(board)
gauges(board)
expose_copper(board)
pcbnew.SaveBoard("pcb_ruler.kicad_pcb", board)
print(len(board.GetDrawings()), "objects")
PCB ruler generated by a Python script in KiCad's PCB Editor: mm and inch ticks on F.Cu, silkscreen numbers, SMD and drill gauges
The result in the PCB Editor: 541 objects, zero clicks.

What matters for manufacturing

Check the result without opening the GUI with kicad-cli pcb drc --exit-code-violations pcb_ruler.kicad_pcb (exit code 0 = no violations), as explained in the headless tutorial.

Black solder mask and 3D render

The mask colour is set in File → Board Setup… → Board Stackup → Physical Stackup, Color column (pick Black for both mask layers and White for the silkscreen), with Copper finish set to ENIG. For a command-line 3D render, tell kicad-cli to use those colours:

kicad-cli pcb render --use-board-stackup-colors --quality high -o ruler.png pcb_ruler.kicad_pcb
Perspective 3D view of the Python-generated PCB ruler: black solder mask, gold copper ticks and drill rings
kicad-cli pcb render using the stackup colours.

The actual colour is chosen when you order from the manufacturer; the stackup colour is mainly for the preview.

Going further

Change range(151) for a 10, 20 or 30 cm ruler (remember to widen the outline), add your logo, or replace the SMD gauge with real footprints loaded with FootprintLoad, as in the placement tutorial.