Added support for frame to `pcb_print` on KiCad 5

- This is tricky because plot API doesn't support it.
  So we use `pcbnew_do` to generate an SVG and then we make a
  vectorized PDF from it.
This commit is contained in:
Salvador E. Tropea 2022-04-11 12:40:39 -03:00
parent a5c848c752
commit 510279a0a3
6 changed files with 130 additions and 29 deletions

View File

@ -1542,6 +1542,7 @@ Next time you need this list just use an alias, like this:
- `pages`: [list(dict)] List of pages to include in the output document.
Each page contains one or more layers of the PCB.
* Valid keys:
- `black_holes`: [boolean=true] Change the drill holes to be black instead of white.
- `exclude_pads_from_silkscreen`: [boolean=false] Do not plot the component pads in the silk screen (KiCad 5.x only).
- `layers`: [list(dict)] List of layers printed in this page. Order is important, the last goes on top.
* Valid keys:
@ -1559,6 +1560,7 @@ Next time you need this list just use an alias, like this:
- `scaling`: [number=1.0] Scale factor (0 means autoscaling).
- `sheet`: [string='Assembly'] Text to use for the `sheet` in the title block.
- `sheet_reference_color`: [string=''] Color to use for the frame and title block.
- `sort_layers`: [boolean=false] Try to sort the layers in the same order that uses KiCad for printing.
- `tent_vias`: [boolean=true] Cover the vias.
- `title`: [string=''] Text used to replace the sheet title. %VALUE expansions are allowed.
If it starts with `+` the text is concatenated.

View File

@ -974,8 +974,10 @@ outputs:
# [list(dict)] List of pages to include in the output document.
# Each page contains one or more layers of the PCB
pages:
# [boolean=false] Do not plot the component pads in the silk screen (KiCad 5.x only)
- exclude_pads_from_silkscreen: false
# [boolean=true] Change the drill holes to be black instead of white
- black_holes: true
# [boolean=false] Do not plot the component pads in the silk screen (KiCad 5.x only)
exclude_pads_from_silkscreen: false
# [list(dict)] List of layers printed in this page. Order is important, the last goes on top
layers:
# [string=''] Color used for this layer
@ -1006,6 +1008,8 @@ outputs:
sheet: 'Assembly'
# [string=''] Color to use for the frame and title block
sheet_reference_color: ''
# [boolean=false] Try to sort the layers in the same order that uses KiCad for printing
sort_layers: false
# [boolean=true] Cover the vias
tent_vias: true
# [string=''] Text used to replace the sheet title. %VALUE expansions are allowed.

28
kibot/kicad/patch_svg.py Normal file
View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 Salvador E. Tropea
# Copyright (c) 2022 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
# Note about the size:
# A4 landscape 841.889764 x 595.275591 pt = 297 x 210 mm
# 1 pt = 4/3*px 1 px = 1"/96 => 1 pt = 4/3 * 1"/96 = 4"/288 = 1"/72 (72 dpi)
# 1" = 25.4 mm => 1 pt = 25.4/72 mm = 0.3527777778 mm
import re
from .. import log
logger = log.get_logger()
def patch_svg_file(file, remove_bkg=False):
logger.debug('Patching SVG file `{}`'.format(file))
with open(file, 'rt') as f:
text = f.read()
text = re.sub(r'<svg (.*) width="(.*)" height="(.*)" viewBox="(\S+) (\S+) (\S+) (\S+)"',
r'<svg \1 width="\3" height="\2" viewBox="\4 \5 \7 \6"', text)
if remove_bkg:
text = re.sub(r'<rect.*>', '', text)
else:
text = re.sub(r'<rect x="(\S+)" y="(\S+)" width="(\S+)" height="(\S+)"',
r'<rect x="\1" y="\2" width="\4" height="\3"', text)
with open(file, 'wt') as f:
f.write(text)

View File

@ -7,6 +7,7 @@
# Adapted from: https://gitlab.com/dennevi/Board2Pdf/
# Note: Original code released as Public Domain
import os
import subprocess
from pcbnew import PLOT_CONTROLLER, PLOT_FORMAT_PDF, FromMM
from shutil import rmtree
from tempfile import mkdtemp
@ -15,6 +16,9 @@ from .gs import GS
from .optionable import Optionable
from .out_base import VariantOptions
from .kicad.color_theme import load_color_theme
from .kicad.patch_svg import patch_svg_file
from .misc import CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, PDF_PCB_PRINT
from .kiplot import check_script, exec_with_retry, add_extra_options
from .macros import macros, document, output_class # noqa: F401
from .layer import Layer, get_priority
from . import PyPDF2
@ -23,10 +27,19 @@ from . import log
logger = log.get_logger()
# TODO:
# - Cache de colores
# - Frame en k5?
# - SVG?
# - rsvg-convert -f pdf -o pp.pdf simple_2layer-F_Cu.svg
def _run_command(cmd):
logger.debug('Executing: '+str(cmd))
try:
cmd_output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logger.error('Failed to run %s, error %d', cmd[0], e.returncode)
if e.output:
logger.debug('Output from command: '+e.output.decode())
exit(PDF_PCB_PRINT)
logger.debug('Output from command:\n'+cmd_output.decode())
def hex_to_rgb(value):
@ -255,7 +268,7 @@ class PCB_PrintOptions(VariantOptions):
To use the KiCad 6 default colors select `_builtin_default`.
Usually user colors are stored as `user`, but you can give it another name """
self.plot_sheet_reference = True
""" Include the title-block. Only available on KiCad 6. """
""" Include the title-block """
self.pages = PagesOptions
""" [list(dict)] List of pages to include in the output document.
Each page contains one or more layers of the PCB """
@ -315,6 +328,71 @@ class PCB_PrintOptions(VariantOptions):
def get_targets(self, out_dir):
return [self._parent.expand_filename(out_dir, self.output)]
def clear_edge_cuts(self):
tmp_layer = GS.board.GetLayerID(GS.work_layer)
edge = GS.board.GetLayerID('Edge.Cuts')
moved = []
for g in GS.board.GetDrawings():
if g.GetLayer() == edge:
g.SetLayer(tmp_layer)
moved.append(g)
for m in GS.get_modules():
for gi in m.GraphicalItems():
if gi.GetLayer() == edge:
gi.SetLayer(tmp_layer)
moved.append(gi)
self.moved_items = moved
self.edge_layer = edge
def restore_edge_cuts(self):
for g in self.moved_items:
g.SetLayer(self.edge_layer)
def plot_frame_ki6(self, pc, po, p):
""" KiCad 6 can plot the frame because it loads the worksheet format """
self.clear_edge_cuts()
po.SetPlotFrameRef(True)
po.SetScale(1.0)
po.SetNegative(False)
pc.SetLayer(self.edge_layer)
pc.OpenPlotfile('frame', PLOT_FORMAT_PDF, p.sheet)
pc.PlotLayer()
self.restore_edge_cuts()
def plot_frame_ki5(self, dir_name):
""" KiCad 5 crashes if we try to print the frame.
So we print a frame using pcbnew_do export.
We use SVG output to then generate a vectorized PDF. """
output = os.path.join(dir_name, GS.pcb_basename+"-frame.svg")
check_script(CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, '1.6.7')
# Move all the drawings away
# KiCad 5 always prints Edge.Cuts, so we make it empty
self.clear_edge_cuts()
# Save the PCB
pcb_name, pcb_dir = self.save_tmp_dir_board('pcb_print')
# Restore the layer
self.restore_edge_cuts()
# Output file name
cmd = [CMD_PCBNEW_PRINT_LAYERS, 'export', '--output_name', output, '--monochrome', '--svg',
pcb_name, dir_name, 'Edge.Cuts']
cmd, video_remove = add_extra_options(cmd)
# Execute it
ret = exec_with_retry(cmd)
# Remove the temporal PCB
logger.debug('Removing temporal PCB used for frame `{}`'.format(pcb_dir))
rmtree(pcb_dir)
if ret:
logger.error(CMD_PCBNEW_PRINT_LAYERS+' returned %d', ret)
exit(PDF_PCB_PRINT)
if video_remove:
video_name = os.path.join(self.expand_filename_pcb(GS.out_dir), 'pcbnew_export_screencast.ogv')
if os.path.isfile(video_name):
os.remove(video_name)
patch_svg_file(output, remove_bkg=True)
# Note: rsvg-convert uses 90 dpi but KiCad (and the docs I found) says SVG pt is 72 dpi
cmd = ['rsvg-convert', '-d', '72', '-p', '72', '-f', 'pdf', '-o', output.replace('.svg', '.pdf'), output]
_run_command(cmd)
def generate_output(self, output):
temp_dir = mkdtemp(prefix='tmp-kibot-pcb_print-')
logger.debug('- Temporal dir: {}'.format(temp_dir))
@ -350,22 +428,19 @@ class PCB_PrintOptions(VariantOptions):
pc.OpenPlotfile(la.suffix, PLOT_FORMAT_PDF, p.sheet)
pc.PlotLayer()
# 2) Plot the frame using an empty layer and 1.0 scale
if self.plot_sheet_reference and GS.ki6():
if self.plot_sheet_reference:
logger.debug('- Plotting the frame')
po.SetPlotFrameRef(True)
po.SetScale(1.0)
po.SetNegative(False)
# TODO: Any better option?
pc.SetLayer(GS.board.GetLayerID(GS.work_layer))
pc.OpenPlotfile('frame', PLOT_FORMAT_PDF, p.sheet)
pc.PlotLayer()
if GS.ki6():
self.plot_frame_ki6(pc, po, p)
else:
self.plot_frame_ki5(temp_dir)
pc.ClosePlot()
# 3) Apply the colors to the layer PDFs
filelist = []
for la in p.layers:
colorize_layer(la.suffix, la.color, p.monochrome, filelist, temp_dir, p.black_holes)
# 4) Apply color to the frame
if self.plot_sheet_reference and GS.ki6():
if self.plot_sheet_reference:
color = p.sheet_reference_color if p.sheet_reference_color else self._color_theme.pcb_frame
colorize_layer('frame', color, p.monochrome, filelist, temp_dir)
# 5) Stack all layers in one file
@ -390,8 +465,7 @@ class PCB_PrintOptions(VariantOptions):
@output_class
class PCB_Print(BaseOutput): # noqa: F821
""" PCB Print
Prints the PCB using a mechanism that is more flexible than `pdf_pcb_print`.
Note that it doesn't support the frame/title block for KiCad 5. """
Prints the PCB using a mechanism that is more flexible than `pdf_pcb_print`. """
def __init__(self):
super().__init__()
with document:

View File

@ -4,10 +4,10 @@
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
import os
import re
from .gs import GS
from .out_any_pcb_print import Any_PCB_PrintOptions
from .error import KiPlotConfigurationError
from .kicad.patch_svg import patch_svg_file
from .macros import macros, document, output_class # noqa: F401
from .layer import Layer
from . import log
@ -34,15 +34,7 @@ class SVG_PCB_PrintOptions(Any_PCB_PrintOptions):
o = self._parent
out_files = o.get_targets(o.expand_dirname(os.path.join(GS.out_dir, o.dir)))
for file in out_files:
logger.debug('Patching SVG file `{}`'.format(file))
with open(file, 'rt') as f:
text = f.read()
text = re.sub(r'<svg (.*) width="(.*)" height="(.*)" viewBox="(\S+) (\S+) (\S+) (\S+)"',
r'<svg \1 width="\3" height="\2" viewBox="\4 \5 \7 \6"', text)
text = re.sub(r'<rect x="(\S+)" y="(\S+)" width="(\S+)" height="(\S+)"',
r'<rect x="\1" y="\2" width="\4" height="\3"', text)
with open(file, 'wt') as f:
f.write(text)
patch_svg_file(file)
@output_class

View File

@ -28,16 +28,17 @@ outputs:
- layer: F.Fab
plot_footprint_refs: false
plot_footprint_values: false
- layer: User.Eco1
- layer: Dwgs.User
- mirror: true
layers:
- layer: B.Fab
color: "#000080"
- layer: Edge.Cuts
color: "#FF8000"
- layer: B.Silkscreen
- layer: B.SilkS
color: "#626262"
- layer: B.Paste
color: "#FF8A8A"
- layer: B.Cu
color: "#B3FFB3"
- layer: Dwgs.User