Advertisement
electrotwelve

Untitled

Feb 6th, 2021
710
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.44 KB | None | 0 0
  1. '''
  2.     A python script to create Nice looking board previews.
  3.  
  4.     These can be used for textures in MCAD tools to cever up the bland STEP board model.
  5. '''
  6.  
  7. import sys
  8. sys.path.insert(
  9.     0,
  10.     "/Applications/Kicad/kicad.app/Contents/Frameworks/python/site-packages/")
  11. import os
  12. import time
  13.  
  14. import re
  15. import logging
  16. import shutil
  17. import subprocess
  18.  
  19. import xml.etree.ElementTree as ET
  20.  
  21.  
  22. from datetime import datetime
  23. from shutil import copy
  24.  
  25. try:
  26.     import pcbnew
  27.     from pcbnew import *
  28. except:
  29.     print("PCBNew not found, are you using KiCAD included Python ?")
  30.     exit()
  31.    
  32.  
  33. greenStandard = {
  34.     'Copper' : ['#E8D959',0.85],
  35.     'CopperInner' : ['#402400',0.80],
  36.     'SolderMask' : ['#1D5D17',0.80],
  37.     # 'Paste' : ['#9E9E9E',0.95],
  38.     'Silk' : ['#eaebe5',1.00],
  39.     'Edge' : ['#000000',0.20],
  40.     'BackGround' : ['#998060']
  41. }
  42.  
  43. oshPark = {
  44.     'Copper' : ['#E8D959',0.85],
  45.     'SolderMask' : ['#3a0e97',0.83],
  46.     'Paste' : ['#9E9E9E',0.05],
  47.     'Silk' : ['#d8dae7',1.00],
  48.     'Edge' : ['#000000',0.20],
  49.     'BackGround' : ['#3a0e97']
  50. }
  51.  
  52. # Black and white colours to be used for texture/bump mapping
  53. bumpMap = {
  54.     'Copper' : ['#666',0.85],
  55.     'SolderMask' : ['#777',0.80],
  56.     'Paste' : ['#FFF',0.95],
  57.     'Silk' : ['#bbb',1.00],
  58.     'Edge' : ['#eeeeee',0.20],
  59.     'BackGround' : ['#555555']
  60. }
  61.  
  62. colours = greenStandard
  63.  
  64. def unique_prefix():
  65.     unique_prefix.counter += 1
  66.     return "pref_" + str(unique_prefix.counter)
  67. unique_prefix.counter = 0
  68.  
  69. def ki2dmil(val):
  70.     return val / 2540
  71.  
  72. def kiColour(val):
  73.     return (val & 0xFF0000 >> 24) / 255
  74.  
  75.  
  76. class svgObject(object):
  77.     # Create a Blank SVG
  78.     def __init__(self, pcb, mirror=False):
  79.         self.bb = pcb.GetBoardEdgesBoundingBox()
  80.         self.mirror = mirror
  81.         self.et = ET.ElementTree(ET.fromstring("""<svg width="29.7002cm" height="21.0007cm" viewBox="0 0 116930 82680 ">
  82. <title>Picutre generated by pcb2svg</title>
  83. <desc>Picture generated by pcb2svg</desc>
  84. <defs> </defs>
  85. </svg>"""))
  86.         self.svg = self.et.getroot()
  87.         defs = self.svg.find('defs')
  88.  
  89.         newMask = ET.SubElement(defs,'mask', id="boardMask",
  90.         width="{}".format(ki2dmil(self.bb.GetWidth())),
  91.         height="{}".format(ki2dmil(self.bb.GetHeight())),
  92.         x="{}".format(ki2dmil(self.bb.GetX())),
  93.         y="{}".format(ki2dmil(self.bb.GetY())))
  94.         if self.mirror:
  95.             newMask.attrib['transform'] = "scale(-1,1)"
  96.        
  97.         rect = ET.SubElement(newMask, 'rect',  
  98.         width="{}".format(ki2dmil(self.bb.GetWidth())),
  99.         height="{}".format(ki2dmil(self.bb.GetHeight())),
  100.         x="{}".format(ki2dmil(self.bb.GetX())),
  101.         y="{}".format(ki2dmil(self.bb.GetY())),
  102.         style="fill:#FFFFFF; fill-opacity:1.0;")
  103.  
  104.     # Open an SVG file
  105.     def openSVG(self, filename):
  106.         prefix = unique_prefix() + "_"
  107.         root = ET.parse(filename)
  108.        
  109.         # We have to ensure all Ids in SVG are unique. Let's make it nasty by
  110.         # collecting all ids and doing search & replace
  111.         # Potentially dangerous (can break user text)
  112.         ids = []
  113.         for el in root.iter():
  114.             if "id" in el.attrib and el.attrib["id"] != "origin":
  115.                 ids.append(el.attrib["id"])
  116.         with open(filename) as f:
  117.             content = f.read()
  118.         for i in ids:
  119.             content = content.replace("#"+i, "#" + prefix + i)
  120.  
  121.         root = ET.fromstring(content)
  122.         # Remove SVG namespace to ease our lifes and change ids
  123.         for el in root.iter():
  124.             if "id" in el.attrib and el.attrib["id"] != "origin":
  125.                 el.attrib["id"] = prefix + el.attrib["id"]
  126.             if '}' in str(el.tag):
  127.                 el.tag = el.tag.split('}', 1)[1]
  128.         self.svg = root
  129.  
  130.  
  131.  
  132.        
  133.  
  134.  
  135.     # Wrap all image data into a group and return that group
  136.     def extractImageAsGroup(self):
  137.         wrapper = ET.Element('g',
  138.         width="{}".format(ki2dmil(self.bb.GetWidth())),
  139.         height="{}".format(ki2dmil(self.bb.GetHeight())),
  140.         x="{}".format(ki2dmil(self.bb.GetX())),
  141.         y="{}".format(ki2dmil(self.bb.GetY())),
  142.         style="fill:#000000; fill-opacity:1.0; stroke:#000000; stroke-opacity:1.0;")
  143.         wrapper.extend(self.svg.iter('g'))
  144.         return wrapper
  145.  
  146.     def reColour(self, transform_function):
  147.         wrapper = self.extractImageAsGroup()
  148.         # Set fill and stroke on all groups
  149.         for group in wrapper.iter():
  150.             svgObject._apply_transform(group, {
  151.                 'fill': transform_function,
  152.                 'stroke': transform_function,
  153.             })
  154.         self.svg = wrapper
  155.  
  156.     @staticmethod
  157.     def _apply_transform(node, values):
  158.         try:
  159.             original_style = node.attrib['style']
  160.             for (k,v) in values.items():
  161.                 escaped_key = re.escape(k)
  162.                 m = re.search(r'\b' + escaped_key + r':(?P<value>[^;]*);', original_style)
  163.                 if m:
  164.                     transformed_value = v
  165.                     original_style = re.sub(
  166.                         r'\b' + escaped_key + r':[^;]*;',
  167.                         k + ':' + transformed_value + ';',
  168.                         original_style)
  169.             node.attrib['style'] = original_style
  170.         except Exception as e:
  171.             style_string = " "
  172.             node.attrib['style'] = style_string
  173.             pass
  174.  
  175.    
  176.    
  177.     def addholes(self, holeData):
  178.         self.svg.append(holeData)
  179.         if self.mirror:
  180.             holeData.attrib['transform'] = "scale(-1,1)"
  181.         #holeData.attrib['mask'] =  "url(#boardMask);"
  182.  
  183.     def addSvgImageInvert(self, svgImage, colour):
  184.         defs = self.svg.find('defs')
  185.         newMask = ET.SubElement(defs,'mask', id="test-a",
  186.         width="{}".format(ki2dmil(self.bb.GetWidth())),
  187.         height="{}".format(ki2dmil(self.bb.GetHeight())),
  188.         x="{}".format(ki2dmil(self.bb.GetX())),
  189.         y="{}".format(ki2dmil(self.bb.GetY())))
  190.         if self.mirror:
  191.             newMask.attrib['transform'] = "scale(-1,1)"
  192.        
  193.  
  194.  
  195.         rect = ET.SubElement(newMask, 'rect',  
  196.         width="{}".format(ki2dmil(self.bb.GetWidth())),
  197.         height="{}".format(ki2dmil(self.bb.GetHeight())),
  198.         x="{}".format(ki2dmil(self.bb.GetX())),
  199.         y="{}".format(ki2dmil(self.bb.GetY())),
  200.         style="fill:#FFFFFF; fill-opacity:1.0;")
  201.  
  202.  
  203.         imageGroup = svgImage.extractImageAsGroup()
  204.         newMask.append(imageGroup)
  205.        
  206.         #create a rectangle to mask through
  207.         wrapper = ET.SubElement(self.svg, 'g',
  208.         style="fill:{}; fill-opacity:0.75;".format(colour))
  209.         rect = ET.SubElement(wrapper, 'rect',
  210.         width="{}".format(ki2dmil(self.bb.GetWidth())),
  211.         height="{}".format(ki2dmil(self.bb.GetHeight())),
  212.         x="{}".format(ki2dmil(self.bb.GetX())),
  213.         y="{}".format(ki2dmil(self.bb.GetY())))
  214.  
  215.  
  216.         wrapper.attrib['mask'] =  "url(#test-a);"
  217.  
  218.         if self.mirror:
  219.             wrapper.attrib['transform'] = "scale(-1,1)"
  220.  
  221.     def addSvgImage(self, svgImage, colour, nofill=False):
  222.        
  223.         #create a rectangle to mask through
  224.         wrapper = ET.SubElement(self.svg, 'g')
  225.        
  226.         imageGroup = svgImage.extractImageAsGroup()
  227.         wrapper.append(imageGroup)
  228.  
  229.         for group in imageGroup.iter():
  230.             svgObject._apply_transform(group, {
  231.                 'fill': colour,
  232.                 'stroke': colour,
  233.             })
  234.             if nofill:
  235.                 if 'stroke-width:0.000394;' not in group.attrib['style']:
  236.                     svgObject._apply_transform(group, {
  237.                         'fill-opacity': "0.0",
  238.                     })
  239.         if self.mirror:
  240.             wrapper.attrib['transform'] = "scale(-1,1)"
  241.  
  242.  
  243.  
  244.     def write(self, filename):
  245.         with open(filename, 'wb') as output_file:
  246.             self.et.write(output_file)
  247.  
  248.  
  249.  
  250.  
  251. def get_hole_mask(board):
  252.     mask = ET.Element( "g", id="hole-mask")
  253.     container = ET.SubElement(mask, "g", style="opacity:0.9;")
  254.  
  255.     # Print all Drills
  256.     for mod in board.GetModules():
  257.         for pad in mod.Pads():
  258.             pos = pad.GetPosition()
  259.             pos_x = ki2dmil(pos.x)
  260.             pos_y = ki2dmil(pos.y)
  261.             size = ki2dmil(min(pad.GetDrillSize())) # Tracks will fail with Get Drill Value
  262.  
  263.             length = 1
  264.             if pad.GetDrillSize()[0] != pad.GetDrillSize()[1]:
  265.                 length = ki2dmil(max(pad.GetDrillSize()) - min(pad.GetDrillSize()))
  266.  
  267.             #length = 200
  268.             stroke = size
  269.             #print(str(size) + " " +  str(length) + " " + str(pad.GetOrientation()))
  270.            
  271.             points = "{} {} {} {}".format(0, -length / 2, 0, length / 2)
  272.             if pad.GetDrillSize()[0] >= pad.GetDrillSize()[1]:
  273.                 points = "{} {} {} {}".format(length / 2, 0, -length / 2, 0)
  274.             el = ET.SubElement(container, "polyline")
  275.             el.attrib["stroke-linecap"] = "round"
  276.             el.attrib["stroke"] = "black"
  277.             el.attrib["stroke-width"] = str(stroke)
  278.             el.attrib["points"] = points
  279.             el.attrib["transform"] = "translate({} {})".format(
  280.                 pos_x, pos_y)  
  281.             el.attrib["transform"] += "rotate({})".format(
  282.                 -pad.GetOrientation()/10)  
  283.  
  284.  
  285.     # Print all Vias
  286.     for track in board.GetTracks():
  287.         if track.GetClass() == "VIA":
  288.             track = Cast_to_VIA(track)
  289.             pos = track.GetPosition()
  290.             pos_x = ki2dmil(pos.x)
  291.             pos_y = ki2dmil(pos.y)
  292.             size = ki2dmil(track.GetDrill()) # Tracks will fail with Get Drill Value
  293.  
  294.             stroke = size
  295.             length = 1
  296.            
  297.             points = "{} {} {} {}".format(0, -length / 2, 0, length / 2)
  298.             el = ET.SubElement(container, "polyline")
  299.             el.attrib["stroke-linecap"] = "round"
  300.             el.attrib["stroke"] = "black"
  301.             el.attrib["opacity"] = "1.0"
  302.             el.attrib["stroke-width"] = str(stroke)
  303.             el.attrib["points"] = points
  304.             el.attrib["transform"] = "translate({} {})".format(
  305.                 pos_x, pos_y)
  306.        
  307.        
  308.  
  309.     return mask
  310.  
  311.  
  312. def plot_layer(layer_info, pctl):
  313.     pctl.SetLayer(layer_info[0])
  314.     pctl.OpenPlotfile("", PLOT_FORMAT_SVG, "")
  315.     pctl.PlotLayer()
  316.     time.sleep(0.01)
  317.     pctl.ClosePlot()
  318.     return pctl.GetPlotFileName()
  319.  
  320.  
  321. def render(pcb, plot_plan, output_filename, mirror=False):
  322.     bb = pcb.GetBoardEdgesBoundingBox()
  323.  
  324.     pctl = PLOT_CONTROLLER(pcb)
  325.  
  326.  
  327.     popt = pctl.GetPlotOptions()
  328.  
  329.     # Set some important plot options:
  330.     popt.SetPlotFrameRef(False)
  331.  
  332.     popt.SetAutoScale(False)
  333.     popt.SetPlotViaOnMaskLayer(False)
  334.    
  335.     kicad_version = 5
  336.     try:
  337.         popt.SetLineWidth(FromMM(0.35))
  338.     except:
  339.         kicad_version = 6
  340.  
  341.     popt.SetAutoScale(False)
  342.     popt.SetMirror(False)
  343.     popt.SetUseGerberAttributes(False)
  344.     popt.SetExcludeEdgeLayer(True);
  345.  
  346.     popt.SetScale(1)
  347.     popt.SetUseAuxOrigin(False)
  348.     if kicad_version == 6:
  349.         popt.SetScale(1/2540)
  350.         popt.SetUseAuxOrigin(True)
  351.        
  352.     popt.SetMirror(False)
  353.     popt.SetUseGerberAttributes(False)
  354.     popt.SetExcludeEdgeLayer(True)
  355.     popt.SetNegative(False)
  356.     popt.SetPlotReference(True)
  357.     popt.SetPlotValue(True)
  358.     popt.SetPlotInvisibleText(False)
  359.     popt.SetDrillMarksType(PCB_PLOT_PARAMS.FULL_DRILL_SHAPE)
  360.     pctl.SetColorMode(True)
  361.  
  362.     pcb_file_dir = os.path.dirname(pcb.GetFileName())
  363.     pcb_file_name = os.path.basename(pcb.GetFileName())
  364.    
  365.  
  366.     output_directory = os.path.join(pcb_file_dir,'plot')
  367.     temp_dir = os.path.join(output_directory, 'temp')
  368.  
  369.     popt.SetOutputDirectory(temp_dir)
  370.     popt.SetSubtractMaskFromSilk(False) #remove solder mask from silk to be sure there is no silk on pads  
  371.  
  372.     canvas = svgObject(pcb, mirror)
  373.     for layer_info in plot_plan:
  374.  
  375.         plot_layer(layer_info, pctl)
  376.        
  377.         svgData = svgObject(pcb, mirror)
  378.         svgData.openSVG(pctl.GetPlotFileName())
  379.  
  380.         if layer_info[1] == "Invert":
  381.             canvas.addSvgImageInvert(svgData, colours[layer_info[2]][0]);
  382.         else:
  383.             if layer_info[2] == 'Silk':
  384.                 canvas.addSvgImage(svgData,colours[layer_info[2]][0], nofill=True)
  385.             else:
  386.                 canvas.addSvgImage(svgData,colours[layer_info[2]][0])
  387.  
  388.     # Drills are seperate from Board layers. Need to be handled differently
  389.     canvas.addholes(get_hole_mask(pcb))
  390.  
  391.  
  392.     print('Merging layers...')
  393.     final_svg = os.path.join(temp_dir, output_filename + '-merged.svg')
  394.     canvas.write(final_svg)
  395.  
  396.     print('Rasterizing...')
  397.     final_png = os.path.join(output_directory, output_filename)
  398.  
  399.     # x0,y0 are bottom LEFT corner
  400.     dpi = 1800
  401.  
  402.     scale = 3.779
  403.     mmscale = 1000000.0
  404.  
  405.     yMax = 210070000
  406.  
  407.     x0 = (canvas.bb.GetX() / mmscale) * scale
  408.     y0 = ((yMax - (canvas.bb.GetY() + canvas.bb.GetHeight())) / mmscale) * scale
  409.     x1 = ((canvas.bb.GetX() + canvas.bb.GetWidth()) / mmscale) * scale
  410.     y1 = ((yMax - (canvas.bb.GetY())) / mmscale) * scale
  411.  
  412.     #x0 -= 10
  413.     #y0 -= 10
  414.     #x1 += 10
  415.     #y1 += 10
  416.  
  417.     if canvas.mirror:
  418.         x0 = -x0
  419.         x1 = -x1
  420.  
  421.     # Hack your path to add a bunch of plausible locations for inkscape
  422.     pathlist = [
  423.         '/Applications/Inkscape.app/Contents/MacOS/inkscape',
  424.         '/Applications/Inkscape.app/Contents/Resources/bin/inkscape',
  425.         'C:\\Program Files\\Inkscape',
  426.         'C:\\Program Files (x86)\\Inkscape',
  427.         '/usr/local/bin',
  428.         '/usr/bin/'
  429.     ]
  430.     os.environ["PATH"] += os.pathsep + os.pathsep.join(pathlist)
  431.     try:   
  432.         version = subprocess.check_output(['inkscape', '--version'], stderr=None).split()
  433.         if (len(version) > 1 and version[1].decode('utf-8').startswith("0.")) or (len(version) == 0):
  434.             print("Detected Inkscape version < 1.0")
  435.             subprocess.check_call([
  436.                 'inkscape',
  437.                 '--export-area={}:{}:{}:{}'.format(int(x0),int(y0),int(x1),int(y1)),
  438.                 '--export-dpi={}'.format(dpi),
  439.                 '--export-png', final_png,
  440.                 '--export-background', colours['BackGround'][0],
  441.                 final_svg,
  442.             ])
  443.         else:
  444.             print("Detected Inkscape version 1.0+")
  445.             subprocess.check_call([
  446.                 'inkscape',
  447.                 #'--export-area={}:{}:{}:{}'.format(int(x0),int(y0),int(x1),int(y1)),
  448.                 '--export-area-drawing',
  449.                 '--export-dpi={}'.format(dpi),
  450.                 '--export-type=png',
  451.                 '--export-filename={}'.format(final_png),
  452.                 '--export-background', colours['BackGround'][0],
  453.                 '--export-background-opacity={}'.format(255),
  454.                 final_svg,
  455.             ])
  456.     except Exception as e:
  457.         print(e)
  458.        
  459.  
  460.    
  461. def main(pcb, pcb_file_name):
  462.  
  463.     #Slight hack for etree. to remove 'ns0:' from output
  464.     ET.register_namespace('', "http://www.w3.org/2000/svg")
  465.  
  466.  
  467.     #pcb = GetBoard()
  468.  
  469.     pcb_file_dir = os.path.dirname(pcb_file_name)
  470.     pcb_file_name = os.path.basename(pcb_file_name)
  471.  
  472.  
  473.     filename=pcb_file_name
  474.     project_name = os.path.splitext(os.path.split(filename)[1])[0]
  475.     project_path = pcb_file_dir
  476.  
  477.     output_directory = os.path.join(project_path,'plot')
  478.  
  479.     temp_dir = os.path.join(output_directory, 'temp')
  480.     shutil.rmtree(temp_dir, ignore_errors=True)
  481.     try:
  482.         os.makedirs(temp_dir)
  483.     except:
  484.         print('folder exists')
  485.  
  486.     today = datetime.now().strftime('%Y%m%d_%H%M%S')
  487.  
  488.     #board = LoadBoard(filename)
  489.  
  490.    
  491.  
  492.  
  493.     # Plot Various layer to generate Front View
  494.     plot_plan = [
  495.         #( In1_Cu, "",'CopperInner' ),
  496.         ( F_Cu, "",'Copper' ),
  497.         ( F_Mask, 'Invert','SolderMask' ),
  498.         ( F_Paste, "" , 'Paste' ),
  499.         ( F_SilkS, "" ,'Silk' ),
  500.         ( Edge_Cuts, ""  ,'Edge' ),
  501.     ]
  502.  
  503.     render(pcb, plot_plan, project_name + '-Front.png')
  504.  
  505.  
  506.     # Fli layers and generate Back View
  507.     plot_plan = [
  508.     #   ( In2_Cu, "",'CopperInner' ),
  509.         ( B_Cu, "",'Copper' ),
  510.         ( B_Mask, "Invert" ,'SolderMask' ),
  511.         ( B_Paste, "" , 'Paste' ),
  512.         ( B_SilkS, "" ,'Silk' ),
  513.         ( Edge_Cuts, ""  ,'Edge' ),
  514.     ]
  515.     render(pcb, plot_plan, project_name + '-Back.png', mirror=True)
  516.  
  517.     # Experiments to render out various texture maps
  518.     #colours = bumpMap
  519.     #plot_plan = [
  520.     #   ( F_Cu, "",'Copper' ),
  521.     #   ( F_Mask, 'Invert','SolderMask' ),
  522.     #   ( F_Paste, "" , 'Paste' ),
  523.     #   ( F_SilkS, "" ,'Silk' ),
  524.     #   ( Edge_Cuts, ""  ,'Edge' ),
  525.     #]
  526.     #render(plot_plan, project_name + '-Bump.png')
  527.  
  528.     shutil.rmtree(temp_dir, ignore_errors=True)
  529.  
  530.  
  531. if __name__ == "__main__":
  532.     filename=sys.argv[1]
  533.     board = LoadBoard(filename)
  534.     main(board, filename)
  535.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement