Standalone pcbnew Python script: plotting a copper layer as monochrome PDF

I needed a way to run a “print”, or rather “plot” of a Pcbnew layout, for individual layers, from the command line. Unfortunately, KiCad/Pcbnew does not have any command line options for that - so I wondered if it would be possible to use Python scripting for that - and it turns out, it would; here I’d like to document some of that. (I’m posting this in Software, because I consider this to be an example of Pcbnew’s existing (scripting) facilities - not a standalone tool)

What confused me at first, was that I had seen:

https://forum.kicad.info/t/tutorials-on-python-scripting-in-pcbnew/5333

These tutorials are great - but they take up the context where you’re running Python code from the Pcbnew interpreter - not as standalone programs from the command line. However, it turns out, that running Python code is quite possible from a standalone script: all one has to do is import sys first, then find the path where the KiCad Python libraries (in particular, pcbnew.py) are – I have KiCad running from an “external”/“build” directory (i.e. it is “portable”, and not installed in system directories), so for me that path is like /path/to/kicad/usr/local/lib/python2.7/dist-packages, YMMV – and then just add this absolute path to Python’s sys.path - then you can import pcbnew.

While you can run most things in this way, you cannot run everything - for instance, if you try to plot a frame reference/worksheet, then there is a C++ function WS_DRAW_ITEM_LIST::BuildFullText called, which calls Pgm().App().GetAppName();, and PCB::Pgm() tries to retrieve a running instance of Pcbnew after checking with assertion; and since here we’re in a standalone script, there is no running instance, and the assertion fails (with a segmentation fault crash on my Ubuntu 14.04). But besides this, everything seems to work.

So I was trying to dig through the code to find the right commands to do a PDF plot, and I would have wasted a lot more hours than I did, if I didn’t find this post:

Python scripting plot options .SetColor() gets ignored : Mailing list archive : kicad-developers team in Launchpad

As unfortunately he didn’t get his question answered, I just wanted to say thanks to “Paul “LeoNerd” Evans” for posting his example - really saved me a lot of time (I would have posted there, too, but it’s kinda hard to necro a mailing list thread :slight_smile: )

Before I go on, this is my KiCad version info:

Application: kicad
Version: (2017-11-13 revision d98fc85)-master, release build
Libraries:
    wxWidgets 3.0.2
    libcurl/7.35.0 OpenSSL/1.0.1f zlib/1.2.8 libidn/1.28 librtmp/2.3
Platform: Linux 4.4.0-109-generic i686, 32 bit, Little endian, wxGTK
Build Info:
    wxWidgets: 3.0.2 (wchar_t,wx containers,compatible with 2.8) GTK+ 2.24
    Boost: 1.54.0
    Curl: 7.35.0
    Compiler: GCC 4.8.4 with C++ ABI 1002

Build settings:
    USE_WX_GRAPHICS_CONTEXT=OFF
    USE_WX_OVERLAY=OFF
    KICAD_SCRIPTING=ON
    KICAD_SCRIPTING_MODULES=ON
    KICAD_SCRIPTING_WXPYTHON=ON
    KICAD_SCRIPTING_ACTION_MENU=ON
    BUILD_GITHUB_PLUGIN=ON
    KICAD_USE_OCE=OFF
    KICAD_SPICE=OFF

Also, below the script, I also include my old non-working code (well, all else works, except for the last PlotWorkSheet command, which I thought was doing the plotting, but it doesn’t) for reference. You can just edit the paths inside, and then run this script with just python test.py (or however you choose to name the script file) - and then you should get a .pdf in the PDF output directory when it completes:

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

import sys
import os

sys.path.append('/path/to/kicad/usr/local/lib/python2.7/dist-packages')

import pcbnew

_INBRDFILEPATH='/path/to/myproj/myproj.kicad_pcb'
_OUTPDFDIRPATH='/tmp'
_PDFFILESUFFIX='papertest-front'
_PLOTSHEETDESC='Papertest (front)' # maybe it's used in FrameRef/Worksheet, but we don't plot that here
_LAYERTOPLOT='F_Cu'
#~ _LAYERTOPLOT='B_Cu'

# this part, started from [Python scripting plot options .SetColor() gets ignored : Mailing list archive : kicad-developers team in Launchpad](https://lists.launchpad.net/kicad-developers/msg25212.html):

board = pcbnew.LoadBoard(_INBRDFILEPATH)

pctl = pcbnew.PLOT_CONTROLLER(board)

popt = pctl.GetPlotOptions()

popt.SetOutputDirectory(_OUTPDFDIRPATH)

# Set some important plot options:
popt.SetPlotFrameRef(False) # if True, assert "process" failed in Pgm(). - and segfault!
popt.SetLineWidth(pcbnew.FromMM(0.1))

popt.SetAutoScale(False)
popt.SetScale(1)
popt.SetMirror(False)
if _LAYERTOPLOT.startswith('B_'): # is a back layer, mirror it
  popt.SetMirror(True)
popt.SetExcludeEdgeLayer(True);

popt.SetPlotReference(True)
popt.SetPlotValue(False)
popt.SetPlotInvisibleText(False)
#popt.SetColor(pcbnew.COLOR4D.BLACK) # no effect either

popt.SetDrillMarksType(pcbnew.PCB_PLOT_PARAMS.NO_DRILL_SHAPE) # this "closes" the holes, so the output (when monochrome) is the same as PDF plot from Pcbnew GUI!

# out filename becomes file-papertest-front.pdf with this:
pctl.OpenPlotfile(_PDFFILESUFFIX, pcbnew.PLOT_FORMAT_PDF, _PLOTSHEETDESC) #  causes: assert "unsigned( aLayer ) < PCB_LAYER_ID_COUNT" failed in ToLAYER_ID(). - but script continues; and completes successfully
pctl.SetColorMode(True)

layers = (
  # note: here, can use pcbnew.F_Cu directly - or can use: getattr(pcbnew, "F_Cu")
  (getattr(pcbnew, _LAYERTOPLOT), pcbnew.COLOR4D.BLACK), #this in .SetLayerColor, only seems to color the F_Cu zones and tracks, not pads for instance (as in pcbnew; there is Layers; and then there is Render, which has "Pads Front", "Pads Back"
  #(pcbnew.B_Cu, pcbnew.COLOR4D.WHITE), # this one will be overlaid on same page!
  (pcbnew.Edge_Cuts, pcbnew.COLOR4D.BLACK),
)

for layer, colour in layers:
  pctl.SetLayer(layer)
  # popt.SetColor(colour)
  #board.GetColorsSettings().SetLayerColor(layer, colour) # AttributeError: GetColorsSettings; note http://docs.kicad.org/doxygen-python/classpcbnew_1_1BOARD.html: "def pcbnew.BOARD.Colors     (           self    )     -> Function GetColorSettings."
  bcols = board.Colors()
  #print(bcols) # <pcbnew.COLORS_DESIGN_SETTINGS; proxy of <Swig Object of type 'COLORS_DESIGN_SETTINGS *' at 0xb706bcc8> >
  bcols.SetLayerColor(layer, colour)
  #popt.SetReferenceColor(colour) # AttributeError: SetReferenceColor
  #popt.SetColor(colour) # no real effect
  bcols.SetAllColorsAs(colour) # works, sets everything to black - including footprint pads!
  pctl.PlotLayer()

pctl.ClosePlot()


"""
# https://kicad.mmccoo.com/2017/02/01/the-basics-of-scripting-in-pcbnew/
# most queries start with a board
#board = pcbnew.GetBoard() # works if it runs in kicad; in standalone script: None
board = pcbnew.LoadBoard(_INBRDFILEPATH) # ok, loads in standalone script too!

#print(dir(pcbnew))
print(board) # <pcbnew.BOARD; proxy of <Swig Object of type 'BOARD *' at 0xf70854a0> >

# returns a dictionary netcode:netinfo_item
netcodes = board.GetNetsByNetcode()

# list off all of the nets in the board.
for netcode, net in netcodes.items():
  print("netcode {}, name {}".format(netcode, net.GetNetname()))

# see http://docs.kicad.org/doxygen-python/namespacepcbnew.html for more:
# there are these constants in pcbnew.: PlotWorkSheet; and 'PLOT_FORMAT_DXF', 'PLOT_FORMAT_GERBER', 'PLOT_FORMAT_HPGL', 'PLOT_FORMAT_PDF', 'PLOT_FORMAT_POST', 'PLOT_FORMAT_SVG', 'PLOT_LAST_FORMAT',
# also: 'ID_GEN_PLOT_PDF', 'PDF_PLOTTER', 'PDF_PLOTTER_GetDefaultFileExtension', 'PDF_PLOTTER_swigregister', 'PLOT_FORMAT_PDF',
# PlotWorkSheet(PLOTTER plotter, TITLE_BLOCK aTitleBlock, PAGE_INFO const & aPageInfo, int aSheetNumber, int aNumberOfSheets, wxString aSheetDesc, wxString aFilename)

pdfplotter = pcbnew.PDF_PLOTTER()
print("type: {} ext: {}".format( pdfplotter.GetPlotterType(), pdfplotter.GetDefaultFileExtension() )) # type: 4 ext: pdf

# line width: default in pcbnew dialog: 0.1 mm; but the function takes integer arg
# http://docs.kicad.org/doxygen-python/classpcbnew_1_1PDF__PLOTTER.html#a8f61f0417dd794ffb6f46e2965b0dd97
result = pdfplotter.OpenFile("/tmp/pcbnew_plot.pdf")
print("OpenFile result: {}".format(result))

#~ # StartPage(), StartPlot() run withoout errors regardless
#~ pdfplotter.StartPage()
#~
#~ # StartPlot: "The PDF engine supports multiple pages; the first one is opened 'for free', the following are to be closed and reopened.; Between each page parameters can be set"
#~ pdfplotter.StartPlot()
#~
#~ # assert "workFile" failed in SetCurrentLineWidth(). or:
#~ # assert "!workFile" failed in StartPage().; startPdfStream().; startPdfObject().
#~ #~ pdfplotter.SetCurrentLineWidth(1)

# NB: workFile is set in PDF_PLOTTER::startPdfStream; kicad_git/common/common_plotPDF_functions.cpp ;; called by PDF_PLOTTER::StartPage
# kicad_git/common/common_plot_functions.cpp:void PlotWorkSheet
# also: plotter->GetColorMode;

pdfplotter.StartPage()

origboardplotopts = board.GetPlotOptions()
print(origboardplotopts) # <pcbnew.PCB_PLOT_PARAMS; proxy of <Swig Object of type 'PCB_PLOT_PARAMS *' at 0xf6ebdcb0> >
#boardpos = board.GetPosition() # 03:05:20: Warning: This should not be called on the BOARD object
#print(boardpos) # (0, 0)
boardtitleblock = board.GetTitleBlock()
print(boardtitleblock) # <pcbnew.TITLE_BLOCK; proxy of <Swig Object of type 'TITLE_BLOCK *' at 0xf33731b8> >
boardpageinfo = board.GetPageSettings()
print(boardpageinfo) # <Swig Object of type 'PAGE_INFO *' at 0xf32aa1d0>

# after StartPage gets rid of assert "workFile"; getting: pcbnew/pcbnew.cpp(203): assert "process" failed in Pgm() ; function: PGM_BASE& Pgm()
# from gdb: aSheetCount=aSheetCount@entry=1, aSheetNumber=aSheetNumber@entry=1;
# however, pcbnew never breaks at PlotWorkSheet for PDF plot!
# there is: pcbnew/dialogs/dialog_plot.cpp:    case PLOT_FORMAT_PDF:
#~ pcbnew.PlotWorkSheet(pdfplotter, boardtitleblock, boardpageinfo, 1, 1, "Desc", _INBRDFILEPATH)
"""

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.