Added support for PcbDraw

This commit is contained in:
Salvador E. Tropea 2020-07-11 13:49:03 -03:00
parent c468dd44e1
commit 52e6bb1b5f
15 changed files with 578 additions and 11 deletions

View File

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- The layers entry is much more flexible now.
Many changes, read the README.md
- PcbDraw output.
- -e/--schematic option to specify any schematic (not just derived from the PCB
name.
- -x/--example option to generate a complete configuration example.

View File

@ -321,7 +321,7 @@ Next time you need this list just use an alias, like this:
- `map`: [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map.
Not generated unless a format is specified.
* Valid keys:
- No available options
- `type`: [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map.
- `metric_units`: [boolean=true] use metric units instead of inches.
- `minimal_header`: [boolean=false] use a minimal header in the file.
- `mirror_y_axis`: [boolean=false] invert the Y axis.
@ -345,7 +345,7 @@ Next time you need this list just use an alias, like this:
- `map`: [dict|string] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map.
Not generated unless a format is specified.
* Valid keys:
- No available options
- `type`: [string='pdf'] [hpgl,ps,gerber,dxf,svg,pdf] format for a graphical drill map.
- `report`: [dict|string] name of the drill report. Not generated unless a name is specified.
* Valid keys:
- `filename`: [string=''] name of the drill report. Not generated unless a name is specified.
@ -448,7 +448,8 @@ Next time you need this list just use an alias, like this:
%r : revision from pcb metadata.
%d : pcb date from metadata if available, file modification date otherwise.
%D : bom generation date.
%T : bom generation time. Extension .html will be added automatically.
%T : bom generation time.
Extension .html will be added automatically.
- `netlist_file`: [string=''] Path to netlist or xml file.
- `no_blacklist_virtual`: [boolean=false] Do not blacklist virtual components.
- `no_redraw_on_drag`: [boolean=false] Do not redraw pcb on drag by default.
@ -479,6 +480,44 @@ Next time you need this list just use an alias, like this:
with a BOM file exported for each variant, separate
variants with the ';' (semicolon) character.
* PcbDraw - Beautiful 2D PCB render
* Type: `pcbdraw`
* Description: Exports the PCB as a 2D model (SVG, PNG or JPG).
Uses configurable colors.
Can also render the components if the 2D models are available
* Valid keys:
- `comment`: [string=''] A comment for documentation purposes.
- `dir`: [string='.'] Output directory for the generated files.
- `name`: [string=''] Used to identify this particular output definition.
- `options`: [dict] Options for the `pcbdraw` output.
* Valid keys:
- `bottom`: [boolean=false] render the bottom side of the board (default is top side).
- `dpi`: [number=300] [10,1200] dots per inch (resolution) of the generated image.
- `format`: [string='svg'] [svg,png,jpg] output format. Only used if no `output` is specified.
- `highlight`: [list(string)] list of components to highlight.
- `libs`: [list(string)] list of libraries.
- `mirror`: [boolean=false] mirror the board.
- `no_drillholes`: [boolean=false] do not make holes transparent.
- `output`: [string='%f-%i.%x'] name for the generated file.
- `placeholder`: [boolean=false] show placeholder for missing components.
- `remap`: [dict|None] replacements for PCB references using components (lib:component).
- `show_components`: [string|list(string)] [none,all] list of components to draw, can be also a string for none or all.
The default is none.
- `style`: [string|dict] PCB style (colors). An internal name, the name of a JSON file or the style options.
* Valid keys:
- `board`: [string='#4ca06c'] color for the board without copper (covered by solder mask).
- `clad`: [string='#9c6b28'] color for the PCB core (not covered by solder mask).
- `copper`: [string='#417e5a'] color for the copper zones (covered by solder mask).
- `highlight_on_top`: [boolean=false] highlight over the component (not under).
- `highlight_padding`: [number=1.5] [0,1000] how much the highlight extends around the component [mm].
- `highlight_style`: [string='stroke:none;fill:#ff0000;opacity:0.5;'] SVG code for the highlight style.
- `outline`: [string='#000000'] color for the outline.
- `pads`: [string='#b5ae30'] color for the exposed pads (metal finish).
- `silk`: [string='#f0f0f0'] color for the silk screen.
- `vcut`: [string='#bf2600'] color for the V-CUTS.
- `vcuts`: [boolean=false] render V-CUTS on the Cmts.User layer.
- `warnings`: [string='visible'] [visible,all,none] using visible only the warnings about components in the visible side are generated.
* PDF (Portable Document Format)
* Type: `pdf`
* Description: Exports the PCB to the most common exhange format. Suitable for printing.

View File

@ -221,7 +221,8 @@ outputs:
# %r : revision from pcb metadata.
# %d : pcb date from metadata if available, file modification date otherwise.
# %D : bom generation date.
# %T : bom generation time. Extension .html will be added automatically
# %T : bom generation time.
# Extension .html will be added automatically
name_format: 'ibom'
# [string=''] Path to netlist or xml file
netlist_file: ''
@ -264,6 +265,64 @@ outputs:
# variants with the ';' (semicolon) character
variant: ''
# PcbDraw - Beautiful 2D PCB render:
# Uses configurable colors.
# Can also render the components if the 2D models are available
- name: 'pcbdraw_example'
comment: 'Exports the PCB as a 2D model (SVG, PNG or JPG).'
type: 'pcbdraw'
dir: 'Example/pcbdraw_dir'
options:
# [boolean=false] render the bottom side of the board (default is top side)
bottom: false
# [number=300] [10,1200] dots per inch (resolution) of the generated image
dpi: 300
# [string='svg'] [svg,png,jpg] output format. Only used if no `output` is specified
format: 'svg'
# [list(string)] list of components to highlight
highlight:
# [list(string)] list of libraries
libs:
# [boolean=false] mirror the board
mirror: false
# [boolean=false] do not make holes transparent
no_drillholes: false
# [string='%f-%i.%x'] name for the generated file
output: '%f-%i.%x'
# [boolean=false] show placeholder for missing components
placeholder: false
# [dict|None] replacements for PCB references using components (lib:component)
remap:
# [string|list(string)] [none,all] list of components to draw, can be also a string for none or all.
# The default is none
show_components:
# [string|dict] PCB style (colors). An internal name, the name of a JSON file or the style options
style:
# [string='#4ca06c'] color for the board without copper (covered by solder mask)
board: '#4ca06c'
# [string='#9c6b28'] color for the PCB core (not covered by solder mask)
clad: '#9c6b28'
# [string='#417e5a'] color for the copper zones (covered by solder mask)
copper: '#417e5a'
# [boolean=false] highlight over the component (not under)
highlight_on_top: false
# [number=1.5] [0,1000] how much the highlight extends around the component [mm]
highlight_padding: 1.5
# [string='stroke:none;fill:#ff0000;opacity:0.5;'] SVG code for the highlight style
highlight_style: 'stroke:none;fill:#ff0000;opacity:0.5;'
# [string='#000000'] color for the outline
outline: '#000000'
# [string='#b5ae30'] color for the exposed pads (metal finish)
pads: '#b5ae30'
# [string='#f0f0f0'] color for the silk screen
silk: '#f0f0f0'
# [string='#bf2600'] color for the V-CUTS
vcut: '#bf2600'
# [boolean=false] render V-CUTS on the Cmts.User layer
vcuts: false
# [string='visible'] [visible,all,none] using visible only the warnings about components in the visible side are generated
warnings: 'visible'
# PDF (Portable Document Format):
# Note that this output isn't the best for documating your project.
# This output is what you get from the File/Plot menu in pcbnew.

View File

@ -175,12 +175,14 @@ def trim(docstring):
def print_output_options(name, cl, indent):
ind_str = indent*' '
obj = cl()
print(ind_str+'* Valid keys:')
num_opts = 0
for k, v in obj.get_attrs_gen():
if k == 'type':
if k == 'type' and indent == 2:
# Type is fixed for an output
continue
if not num_opts:
# We found one, put the title
print(ind_str+'* Valid keys:')
help = getattr(obj, '_help_'+k)
if help is None:
help = 'Undocumented' # pragma: no cover
@ -194,8 +196,8 @@ def print_output_options(name, cl, indent):
num_opts = num_opts+1
if isinstance(v, type):
print_output_options('', v, indent+4)
if num_opts == 0:
print(ind_str+' - No available options')
# if num_opts == 0:
# print(ind_str+' - No available options')
def print_one_out_help(details, n, o):

View File

@ -21,6 +21,7 @@ NO_PCBNEW_MODULE = 16
CORRUPTED_PCB = 17
KICAD2STEP_ERR = 18
WONT_OVERWRITE = 19
PCBDRAW_ERR = 20
CMD_EESCHEMA_DO = 'eeschema_do'
URL_EESCHEMA_DO = 'https://github.com/INTI-CMNB/kicad-automation-scripts'
@ -33,5 +34,6 @@ URL_KIBOM = 'https://github.com/INTI-CMNB/KiBoM'
CMD_IBOM = 'generate_interactive_bom.py'
URL_IBOM = 'https://github.com/INTI-CMNB/InteractiveHtmlBom'
KICAD2STEP = 'kicad2step'
PCBDRAW = 'pcbdraw'
EXAMPLE_CFG = 'example.kiplot.yaml'
AUTO_SCALE = 0

View File

@ -1,6 +1,10 @@
import os
import re
import inspect
from re import (compile)
from re import compile
from datetime import datetime
from .error import KiPlotConfigurationError
from .gs import GS
from . import log
logger = log.get_logger(__name__)
@ -138,6 +142,39 @@ class Optionable(object):
attrs = self.get_attrs_for()
return ((k, v) for k, v in attrs.items() if k[0] != '_')
def expand_filename(self, name, id='', ext=''):
""" Expands %x values in filenames """
if GS.board:
# This is based on InterativeHtmlBom expansion
title_block = GS.board.GetTitleBlock()
file_date = title_block.GetDate()
if not file_date:
file_mtime = os.path.getmtime(GS.pcb_file)
file_date = datetime.fromtimestamp(file_mtime).strftime('%Y-%m-%d_%H-%M-%S')
pcb_file_name = os.path.splitext(os.path.basename(GS.pcb_file))[0]
title = title_block.GetTitle()
if not title:
title = pcb_file_name
revision = title_block.GetRevision()
company = title_block.GetCompany()
n = datetime.now()
today = n.strftime('%Y-%m-%d')
time = n.strftime('%H-%M-%S')
# Do the replacements
name = name.replace('%f', pcb_file_name)
name = name.replace('%p', title)
name = name.replace('%c', company)
name = name.replace('%r', revision)
name = name.replace('%d', file_date)
name = name.replace('%D', today)
name = name.replace('%T', time)
name = name.replace('%i', id)
name = name.replace('%x', ext)
# sanitize the name to avoid characters illegal in file systems
name = name.replace('\\', '/')
name = re.sub(r'[?%*:|"<>]', '_', name)
return name
class BaseOptions(Optionable):
""" A class to validate and hold output options.

View File

@ -42,7 +42,8 @@ class IBoMOptions(BaseOptions):
%r : revision from pcb metadata.
%d : pcb date from metadata if available, file modification date otherwise.
%D : bom generation date.
%T : bom generation time. Extension .html will be added automatically """
%T : bom generation time.
Extension .html will be added automatically """
self.include_tracks = False
""" Include track/zone information in output. F.Cu and B.Cu layers only """
self.include_nets = False

245
kiplot/out_pcbdraw.py Normal file
View File

@ -0,0 +1,245 @@
import os
import re
from tempfile import (NamedTemporaryFile)
from subprocess import (check_output, STDOUT, CalledProcessError)
from .misc import (PCBDRAW, PCBDRAW_ERR)
from .error import KiPlotConfigurationError
from .gs import (GS)
from .optionable import (BaseOptions, Optionable)
from kiplot.macros import macros, document, output_class # noqa: F401
from . import log
logger = log.get_logger(__name__)
class PcbDrawStyle(Optionable):
_color_re = re.compile(r"#[A-Fa-f0-9]{6}$")
def __init__(self):
super().__init__()
with document:
self.copper = "#417e5a"
""" color for the copper zones (covered by solder mask) """
self.board = "#4ca06c"
""" color for the board without copper (covered by solder mask) """
self.silk = "#f0f0f0"
""" color for the silk screen """
self.pads = "#b5ae30"
""" color for the exposed pads (metal finish) """
self.outline = "#000000"
""" color for the outline """
self.clad = "#9c6b28"
""" color for the PCB core (not covered by solder mask) """
self.vcut = "#bf2600"
""" color for the V-CUTS """
self.highlight_on_top = False
""" highlight over the component (not under) """
self.highlight_style = "stroke:none;fill:#ff0000;opacity:0.5;"
""" SVG code for the highlight style """
self.highlight_padding = 1.5
""" [0,1000] how much the highlight extends around the component [mm] """ # pragma: no cover
def validate_color(self, name):
color = getattr(self, name)
if not self._color_re.match(color):
raise KiPlotConfigurationError('Invalid color for `{}` use `#rrggbb` with hex digits'.format(name))
def config(self, tree):
super().config(tree)
self.validate_color('board')
self.validate_color('copper')
self.validate_color('board')
self.validate_color('silk')
self.validate_color('pads')
self.validate_color('outline')
self.validate_color('clad')
self.validate_color('vcut')
# Not implemented but required
self.highlight_offset = 0
def to_dict(self):
return {k.replace('_', '-'): v for k, v in self.get_attrs_gen()}
class PcbDrawRemap(Optionable):
""" This class accepts a free form dict.
No validation is done. """
def __init__(self):
super().__init__()
def config(self, tree):
self._tree = tree
class PcbDrawOptions(BaseOptions):
def __init__(self):
super().__init__()
with document:
self.style = PcbDrawStyle
""" [string|dict] PCB style (colors). An internal name, the name of a JSON file or the style options """
self.libs = Optionable
""" [list(string)] list of libraries """
self.placeholder = False
""" show placeholder for missing components """
self.remap = PcbDrawRemap
""" [dict|None] replacements for PCB references using components (lib:component) """
self.no_drillholes = False
""" do not make holes transparent """
self.bottom = False
""" render the bottom side of the board (default is top side) """
self.mirror = False
""" mirror the board """
self.highlight = Optionable
""" [list(string)] list of components to highlight """
self.show_components = Optionable
""" [string|list(string)] [none,all] list of components to draw, can be also a string for none or all.
The default is none """
self.vcuts = False
""" render V-CUTS on the Cmts.User layer """
self.warnings = 'visible'
""" [visible,all,none] using visible only the warnings about components in the visible side are generated """
self.dpi = 300
""" [10,1200] dots per inch (resolution) of the generated image """
self.format = 'svg'
""" [svg,png,jpg] output format. Only used if no `output` is specified """
self.output = '%f-%i.%x'
""" name for the generated file """ # pragma: no cover
def config(self, tree):
super().config(tree)
# Libs
if isinstance(self.libs, type):
self.libs = None
else:
self.libs = ','.join(self.libs)
# Highlight
if isinstance(self.highlight, type):
self.highlight = None
else:
self.highlight = ','.join(self.highlight)
# Filter
if isinstance(self.show_components, type):
self.show_components = ''
elif isinstance(self.show_components, str):
if self.show_components == 'none':
self.show_components = ''
else:
self.show_components = None
else:
self.show_components = ','.join(self.show_components)
# Remap
if isinstance(self.remap, type):
self.remap = None
elif isinstance(self.remap, PcbDrawRemap):
self.remap = self.remap._tree
# Style
if isinstance(self.style, type):
self.style = None
elif isinstance(self.style, PcbDrawStyle):
self.style = self.style.to_dict()
def _create_remap(self):
with NamedTemporaryFile(mode='w', delete=False) as f:
f.write('{\n')
first = True
for k, v in self.remap.items():
if first:
first = False
else:
f.write(',\n')
f.write(' "{}": "{}"'.format(k, v))
f.write('\n}\n')
f.close()
return f.name
def _create_style(self):
with NamedTemporaryFile(mode='w', delete=False) as f:
f.write('{\n')
first = True
for k, v in self.style.items():
if first:
first = False
else:
f.write(',\n')
if isinstance(v, str):
f.write(' "{}": "{}"'.format(k, v))
elif isinstance(v, bool):
f.write(' "{}": {}'.format(k, str(v).lower()))
else:
f.write(' "{}": {}'.format(k, v))
f.write('\n}\n')
f.close()
return f.name
def run(self, output_dir, board):
# Output file name
output = self.expand_filename(self.output, 'bottom' if self.bottom else 'top', self.format)
output = os.path.abspath(os.path.join(output_dir, output))
# Base command with overwrite
cmd = [PCBDRAW]
# Add user options
tmp_style = None
if self.style:
if isinstance(self.style, str):
cmd.extend(['-s', self.style])
else:
tmp_style = self._create_style()
cmd.extend(['-s', tmp_style])
if self.libs:
cmd.extend(['-l', self.libs])
if self.placeholder:
cmd.append('--placeholder')
if self.no_drillholes:
cmd.append('--no-drillholes')
if self.bottom:
cmd.append('-b')
if self.mirror:
cmd.append('--mirror')
if self.highlight:
cmd.extend(['-a', self.highlight])
if self.show_components is not None:
cmd.extend(['-f', self.show_components])
if self.vcuts:
cmd.append('-v')
if self.warnings == 'all':
cmd.append('--warn-back')
elif self.warnings == 'none':
cmd.append('--silent')
if self.dpi:
cmd.extend(['--dpi', str(self.dpi)])
if self.remap:
tmp_remap = self._create_remap()
cmd.extend(['-m', tmp_remap])
else:
tmp_remap = None
# The board & output
cmd.append(GS.pcb_file)
cmd.append(output)
# Execute and inform is successful
logger.debug('Executing: '+str(cmd))
try:
cmd_output = check_output(cmd, stderr=STDOUT)
except CalledProcessError as e:
logger.error('Failed to run %s, error %d', PCBDRAW, e.returncode)
if e.output:
logger.debug('Output from command: '+e.output.decode())
exit(PCBDRAW_ERR)
finally:
if tmp_remap:
os.remove(tmp_remap)
if tmp_style:
os.remove(tmp_style)
logger.debug('Output from command:\n'+cmd_output.decode())
@output_class
class PcbDraw(BaseOutput): # noqa: F821
""" PcbDraw - Beautiful 2D PCB render
Exports the PCB as a 2D model (SVG, PNG or JPG).
Uses configurable colors.
Can also render the components if the 2D models are available """
def __init__(self):
super().__init__()
with document:
self.options = PcbDrawOptions
""" [dict] Options for the `pcbdraw` output """ # pragma: no cover

View File

@ -41,7 +41,8 @@ from utils import context
prev_dir = os.path.dirname(prev_dir)
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
from kiplot.misc import (EXIT_BAD_ARGS, EXIT_BAD_CONFIG, NO_PCB_FILE, NO_SCH_FILE, EXAMPLE_CFG, WONT_OVERWRITE, CORRUPTED_PCB)
from kiplot.misc import (EXIT_BAD_ARGS, EXIT_BAD_CONFIG, NO_PCB_FILE, NO_SCH_FILE, EXAMPLE_CFG, WONT_OVERWRITE, CORRUPTED_PCB,
PCBDRAW_ERR)
POS_DIR = 'positiondir'
@ -433,3 +434,11 @@ def test_corrupted_pcb():
ctx.run(CORRUPTED_PCB)
assert ctx.search_err('Error loading PCB file')
ctx.clean_up()
def test_pcbdraw_fail():
prj = 'bom'
ctx = context.TestContext('PcbDrawFail', prj, 'pcbdraw_fail', '')
ctx.run(PCBDRAW_ERR)
assert ctx.search_err('Failed to run')
ctx.clean_up()

View File

@ -0,0 +1,36 @@
"""
Tests for PcbDraw.
For debug information use:
pytest-3 --log-cli-level debug
"""
import os
import sys
# Look for the 'utils' module from where the script is running
prev_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if prev_dir not in sys.path:
sys.path.insert(0, prev_dir)
# Utils import
from utils import context
OUT_DIR = 'PcbDraw'
def test_pcbdraw_3Rs():
prj = '3Rs'
ctx = context.TestContext(OUT_DIR, prj, 'pcbdraw', OUT_DIR)
ctx.run()
ctx.expect_out_file(os.path.join(OUT_DIR, prj+'-top.svg'))
ctx.expect_out_file(os.path.join(OUT_DIR, prj+'-bottom.svg'))
ctx.clean_up()
def test_pcbdraw_simple():
prj = 'bom'
ctx = context.TestContext(OUT_DIR+'_simple', prj, 'pcbdraw_simple', OUT_DIR)
ctx.run()
ctx.expect_out_file(os.path.join(OUT_DIR, prj+'-top.svg'))
ctx.expect_out_file(os.path.join(OUT_DIR, prj+'-bottom.svg'))
ctx.clean_up()

View File

@ -38,6 +38,8 @@ Tests various errors in the config file
- Unknown section
- HPGL wrong pen_number
- KiBoM wrong format
- PcbDraw
- Wrong color
For debug information use:
pytest-3 --log-cli-level debug
@ -429,3 +431,10 @@ def test_error_print_pcb_no_layer():
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Missing .?layers.? list")
ctx.clean_up()
def test_error_color():
ctx = context.TestContext('PrPCB', 'bom', 'error_color', '')
ctx.run(EXIT_BAD_CONFIG)
assert ctx.search_err("Invalid color for .?board.?")
ctx.clean_up()

View File

@ -0,0 +1,22 @@
kiplot:
version: 1
outputs:
- name: PcbDraw
comment: "PcbDraw test top"
type: pcbdraw
dir: PcbDraw
options:
format: svg
style:
board: "bogus"
copper: "#00406a"
silk: "#d5dce4"
pads: "#cfb96e"
clad: "#72786c"
outline: "#000000"
vcut: "#bf2600"
highlight_on_top: false
highlight_style: "stroke:none;fill:#ff0000;opacity:0.5;"
highlight_padding: 1.5

View File

@ -0,0 +1,62 @@
kiplot:
version: 1
outputs:
- name: PcbDraw
comment: "PcbDraw test top"
type: pcbdraw
dir: PcbDraw
options: &pcb_draw_ops
format: svg
style:
board: "#1b1f44"
copper: "#00406a"
silk: "#d5dce4"
pads: "#cfb96e"
clad: "#72786c"
outline: "#000000"
vcut: "#bf2600"
highlight_on_top: false
highlight_style: "stroke:none;fill:#ff0000;opacity:0.5;"
highlight_padding: 1.5
libs:
- default
- eagle-default
remap:
L_G1: "LEDs:LED-5MM_green"
L_B1: "LEDs:LED-5MM_blue"
L_Y1: "LEDs:LED-5MM_yellow"
PHOTO1: "yaqwsx:R_PHOTO_7mm"
J8: "yaqwsx:Pin_Header_Straight_1x02_circle"
'REF**': "dummy:dummy"
G***: "dummy:dummy"
svg2mod: "dummy:dummy"
JP1: "dummy:dummy"
JP2: "dummy:dummy"
JP3: "dummy:dummy"
JP4: "dummy:dummy"
no_drillholes: False
mirror: False
highlight:
- L_G1
- L_B1
- R10
- RV1
show_components: all
vcuts: True
warnings: visible
dpi: 600
- name: PcbDraw
comment: "PcbDraw test bottom"
type: pcbdraw
dir: PcbDraw
options:
<<: *pcb_draw_ops
style: set-red-enig
bottom: True
show_components:
- L_G1
- L_B1
remap:

View File

@ -0,0 +1,16 @@
kiplot:
version: 1
outputs:
- name: PcbDraw
comment: "PcbDraw test top"
type: pcbdraw
dir: PcbDraw
options:
format: svg
style: bogus
no_drillholes: True
placeholder: True
mirror: True
vcuts: True
warnings: all

View File

@ -0,0 +1,27 @@
kiplot:
version: 1
outputs:
- name: PcbDraw
comment: "PcbDraw test top"
type: pcbdraw
dir: PcbDraw
options: &pcb_draw_ops
format: svg
no_drillholes: True
placeholder: True
mirror: True
vcuts: True
warnings: all
- name: PcbDraw
comment: "PcbDraw test bottom"
type: pcbdraw
dir: PcbDraw
options:
<<: *pcb_draw_ops
style: set-red-enig
bottom: True
show_components: none
warnings: none