-
-
Save aster94/bd52972ab6dbf13a44fc046b4222f7e7 to your computer and use it in GitHub Desktop.
import pcbnew | |
import os | |
import shutil | |
import subprocess | |
# SETTINGS: | |
# Gerber | |
# Drill | |
METRIC = True | |
ZERO_FORMAT = pcbnew.GENDRILL_WRITER_BASE.DECIMAL_FORMAT | |
INTEGER_DIGITS = 3 | |
MANTISSA_DIGITS = 3 | |
MIRROR_Y_AXIS = False | |
HEADER = True | |
OFFSET = pcbnew.wxPoint(0,0) | |
MERGE_PTH_NPTH = True | |
DRILL_FILE = True | |
MAP_FILE = False | |
REPORTER = None | |
def generate_gerbers(pcb, path): | |
plot_controller = pcbnew.PLOT_CONTROLLER(pcb) | |
plot_options = plot_controller.GetPlotOptions() | |
# Set General Options: | |
plot_options.SetOutputDirectory(path) | |
plot_options.SetPlotFrameRef(False) | |
plot_options.SetPlotValue(True) | |
plot_options.SetPlotReference(True) | |
plot_options.SetPlotInvisibleText(True) | |
plot_options.SetPlotViaOnMaskLayer(True) | |
plot_options.SetExcludeEdgeLayer(False) | |
#plot_options.SetPlotPadsOnSilkLayer(PLOT_PADS_ON_SILK_LAYER) | |
#plot_options.SetUseAuxOrigin(PLOT_USE_AUX_ORIGIN) | |
plot_options.SetMirror(False) | |
#plot_options.SetNegative(PLOT_NEGATIVE) | |
#plot_options.SetDrillMarksType(PLOT_DRILL_MARKS_TYPE) | |
#plot_options.SetScale(PLOT_SCALE) | |
plot_options.SetAutoScale(True) | |
#plot_options.SetPlotMode(PLOT_MODE) | |
#plot_options.SetLineWidth(pcbnew.FromMM(PLOT_LINE_WIDTH)) | |
# Set Gerber Options | |
#plot_options.SetUseGerberAttributes(GERBER_USE_GERBER_ATTRIBUTES) | |
#plot_options.SetUseGerberProtelExtensions(GERBER_USE_GERBER_PROTEL_EXTENSIONS) | |
#plot_options.SetCreateGerberJobFile(GERBER_CREATE_GERBER_JOB_FILE) | |
#plot_options.SetSubtractMaskFromSilk(GERBER_SUBTRACT_MASK_FROM_SILK) | |
#plot_options.SetIncludeGerberNetlistInfo(GERBER_INCLUDE_GERBER_NETLIST_INFO) | |
plot_plan = [ | |
( 'F.Cu', pcbnew.F_Cu, 'Front Copper' ), | |
( 'B.Cu', pcbnew.B_Cu, 'Back Copper' ), | |
( 'F.Paste', pcbnew.F_Paste, 'Front Paste' ), | |
( 'B.Paste', pcbnew.B_Paste, 'Back Paste' ), | |
( 'F.SilkS', pcbnew.F_SilkS, 'Front SilkScreen' ), | |
( 'B.SilkS', pcbnew.B_SilkS, 'Back SilkScreen' ), | |
( 'F.Mask', pcbnew.F_Mask, 'Front Mask' ), | |
( 'B.Mask', pcbnew.B_Mask, 'Back Mask' ), | |
( 'Edge.Cuts', pcbnew.Edge_Cuts, 'Edges' ), | |
( 'Eco1.User', pcbnew.Eco1_User, 'Eco1 User' ), | |
( 'Eco2.User', pcbnew.Eco2_User, 'Eco1 User' ), | |
] | |
for layer_info in plot_plan: | |
plot_controller.SetLayer(layer_info[1]) | |
plot_controller.OpenPlotfile(layer_info[0], pcbnew.PLOT_FORMAT_GERBER, layer_info[2]) | |
plot_controller.PlotLayer() | |
plot_controller.ClosePlot() | |
def detect_blind_buried_or_micro_vias(pcb): | |
through_vias = 0 | |
micro_vias = 0 | |
blind_or_buried_vias = 0 | |
for track in pcb.GetTracks(): | |
if track.Type() != pcbnew.PCB_VIA_T: | |
continue | |
if track.GetShape() == pcbnew.VIA_THROUGH: | |
through_vias += 1 | |
elif track.GetShape() == pcbnew.VIA_MICROVIA: | |
micro_vias += 1 | |
elif track.GetShape() == pcbnew.VIA_BLIND_BURIED: | |
blind_or_buried_vias += 1 | |
if micro_vias or blind_or_buried_vias: | |
return True | |
else: | |
return False | |
def generate_drill_file(pcb, path): | |
#if detect_blind_buried_or_micro_vias(pcb): | |
# return | |
drill_writer = pcbnew.EXCELLON_WRITER(pcb) | |
drill_writer.SetFormat(METRIC, ZERO_FORMAT, INTEGER_DIGITS, MANTISSA_DIGITS) | |
drill_writer.SetOptions(MIRROR_Y_AXIS, HEADER, OFFSET, MERGE_PTH_NPTH) | |
drill_writer.CreateDrillandMapFilesSet(path, DRILL_FILE, MAP_FILE, REPORTER) | |
class SimplePlugin(pcbnew.ActionPlugin): | |
def defaults(self): | |
self.name = 'Gerber Plot' | |
self.category = 'Gerber' | |
self.description = 'Generate Gerber files, drill holes, see the result and send to a compressed folder' | |
self.show_toolbar_button = True | |
self.icon_file_name = os.path.join(os.path.dirname(__file__), 'gerber_plot_icon.png') | |
def Run(self): | |
# The entry function of the plugin that is executed on user action | |
try: | |
cwd_path = os.getcwd() | |
pcb = pcbnew.GetBoard() | |
project_path, project_name = os.path.split(pcb.GetFileName()) | |
project_name = os.path.splitext(project_name)[0] | |
output_path = os.path.join(project_path, project_name + '-Gerber').replace('\\','/') | |
tmp_path = os.path.join(project_path, 'tmp').replace('\\','/') | |
log_file = os.path.join(project_path, 'log.txt').replace('\\','/') | |
if os.path.exists(log_file): | |
os.remove(log_file) | |
except Exception as err: | |
with open(log_file, 'a') as file: | |
file.write('Startup error\nError:{}\n'.format(err)) | |
# Create a temp folder | |
try: | |
os.mkdir(tmp_path) | |
except Exception as err: | |
with open(log_file, 'a') as file: | |
file.write('tmp folder not created\nError:{}\n'.format(err)) | |
# Generate Gerber and drill files | |
try: | |
generate_gerbers(pcb, tmp_path) | |
except Exception as err: | |
with open(log_file, 'a') as file: | |
file.write('Gerbers not plotted\nError:{}\n'.format(err)) | |
try: | |
generate_drill_file(pcb, tmp_path) | |
except Exception as err: | |
with open(log_file, 'a') as file: | |
file.write('Drill file not plotted\nError:{}\n'.format(err)) | |
# Render an image: we need to call an external script that uses python 3 | |
try: | |
subprocess.check_call(['powershell','render_pcb', tmp_path, os.path.join(project_path, project_name + '.png').replace('\\','/')], shell=True) | |
# if you don't wish to have it as a exe file you could use: | |
#subprocess.check_call(['powershell', 'path_to_python3', 'path_to_render_pcb', tmp_path, os.path.join(project_path, project_name + '.png').replace('\\','/')], shell=True) | |
except Exception as err: | |
with open(log_file, 'a') as file: | |
file.write('PCB not rendered\nError:{}\n'.format(err)) | |
# Create compressed file from tmp | |
try: | |
os.chdir(tmp_path) | |
shutil.make_archive(output_path, 'zip', tmp_path) | |
os.chdir(cwd_path) | |
except Exception as err: | |
with open(log_file, 'a') as file: | |
file.write('ZIP file not created\nError:{}\n'.format(err)) | |
# Remove temp folder | |
try: | |
shutil.rmtree(tmp_path, ignore_errors=True) | |
except Exception as err: | |
with open(log_file, 'a') as file: | |
file.write('temp folder not deleted\nError:{}\n'.format(err)) | |
SimplePlugin().register() # Instantiate and register to Pcbnew |
import os, shutil | |
from gerber import common | |
from gerber.layers import PCBLayer, DrillLayer | |
from gerber.render import RenderSettings | |
from gerber.render.cairo_backend import GerberCairoContext | |
from PIL import Image | |
import click | |
# Render | |
SCALE = 25 | |
OFFSET = 20 | |
@click.command() | |
@click.argument('input_path') | |
def render_pcb(input_path): | |
"""Render Gerber Files into a PNG Image | |
INPUT_PATH - Could be a folder or a zip file containing the Gerber Files | |
""" | |
del_tmp_folder = False | |
extract_dir = '' | |
if os.path.isfile(input_path): | |
if not input_path.endswith('.zip'): | |
click.BadParameter('Wrong INPUT_PATH') # exit | |
extract_dir = os.path.join(os.path.dirname(input_path), 'tmp') | |
shutil.unpack_archive(input_path, extract_dir, 'zip') | |
input_path = extract_dir | |
del_tmp_folder = True | |
output_path = os.path.join(os.path.dirname(input_path), 'pcb.png') | |
img_front_path = os.path.join(input_path, 'front.png') | |
img_bottom_path = os.path.join(input_path, 'bottom.png') | |
for file in os.listdir(input_path): | |
real_path = os.path.join(input_path, file) | |
if not os.path.isfile(real_path): | |
continue | |
# Drill | |
if file.endswith('.drl'): | |
drill = DrillLayer(real_path, common.read(real_path)) | |
# Front | |
elif file.endswith('-F_Cu.gbr'): | |
copper_front = PCBLayer(real_path, 'top', common.read(real_path)) | |
elif file.endswith('-F_Mask.gbr'): | |
mask_front = PCBLayer(real_path, 'topmask', common.read(real_path)) | |
elif file.endswith('-F_SilkS.gbr'): | |
silk_front = PCBLayer(real_path, 'topsilk', common.read(real_path)) | |
# Bottom | |
elif file.endswith('-B_Cu.gbr'): | |
copper_bottom = PCBLayer(real_path, 'bottom', common.read(real_path)) | |
elif file.endswith('-B_Mask.gbr'): | |
mask_bottom = PCBLayer(real_path, 'bottommask', common.read(real_path)) | |
elif file.endswith('-B_SilkS.gbr'): | |
silk_bottom = PCBLayer(real_path, 'bottomsilk', common.read(real_path)) | |
else: | |
continue | |
# Create a new drawing context | |
ctx = GerberCairoContext(scale=SCALE) | |
ctx.render_layer(copper_front) | |
ctx.render_layer(mask_front) | |
ctx.render_layer(silk_front) | |
ctx.render_layer(drill) | |
# Write png file | |
ctx.dump(img_front_path) | |
# Clear the drawing | |
ctx.clear() | |
# Render bottom layers | |
ctx.render_layer(copper_bottom) | |
ctx.render_layer(mask_bottom) | |
ctx.render_layer(silk_bottom) | |
ctx.render_layer(drill, settings=RenderSettings(mirror=True)) | |
# Write png file | |
ctx.dump(img_bottom_path) | |
ctx.clear() | |
# Concatenate | |
front = Image.open(img_front_path) | |
bottom = Image.open(img_bottom_path) | |
render = Image.new('RGB', (front.width, front.height * 2 + OFFSET)) | |
render.paste(front, (0, 0)) | |
render.paste(bottom, (0, front.height + OFFSET)) | |
render.save(output_path) | |
render.show() | |
if del_tmp_folder: | |
shutil.rmtree(extract_dir, ignore_errors=True) | |
if __name__ == "__main__": | |
render_pcb() |
... GPL v3 is OK, right? or did you want an other licence?
whatever license fits better for your organization
could it be adjusted tot be used from the command-line?
I am happy you made it work!
Keep in mind this: the two files (render_pcb.py and plot_gerber.py) are split for a reason. Kicad uses an old version of python (2.7) and Cairo (a library needed to generate the png image) is not well supported by this old python. So I had to call a newer version of python (which support cairo) from the main file
here the code: https://gist.github.com/aster94/bd52972ab6dbf13a44fc046b4222f7e7#file-gerber_plot-py-L151-L155
once the kicad team will move to the newer python you will be able to merge the two files in one (here a better explanation of the issue https://forum.kicad.info/t/run-kicads-python-pip/20381/4)
@hoijui check also the idea of @hildogjr https://gist.github.com/aster94/bd52972ab6dbf13a44fc046b4222f7e7#gistcomment-3122252
ouhh thank you for that explanation!
I will add this as docu into the file.
That only seems to affect the gerber_plot.py when used as a plugin in KiCad though (which I did not try). When using from the command line, I ran both the files with my default python (3.8.5), and it works fine.
I left the plugin part in there anyway, even though we do not need it; I will add this comment of yours to that part of the code.
IMHO you should merge the two files and remove the kicad part, it will make life for future contributor easier
you just need to bring the function render_pcb
in the main file and instead of this (https://gist.github.com/aster94/bd52972ab6dbf13a44fc046b4222f7e7#file-gerber_plot-py-L153) call the new function
I personally would not merge the files, because they do different, potentially independent usable things. So to keep it modular, having them separate makes sense.
For example, I just got to know about KiBot, which seems to be a much more sophisticated and further developed way to auto-generate Gerbers from KiCad files. If I see it right though, it does not do 2D rendering of Gerbers, so we could use KiBot for gerbers and your render_pcb.py
for 2D rendering.
I see it can generate also BOM and pick and place files, very nice!
yeah!
It can also render in 2D, I saw
I did not yet get it to work for me. it is made up of quite a few sub-tools, and one needs to find out how to install each one first. I will try to help make the documentation more easy to get into. But the end result is really great! especially the interactive BOM.
I put something together that basically seems to work now:
https://github.com/osegermany/kicad-pcb-generate-doc-tool
... GPL v3 is OK, right? or did you want an other licence?