KiBot/kibot/out_pcb_print.py

1288 lines
58 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 Salvador E. Tropea
# Copyright (c) 2022-2023 Instituto Nacional de Tecnología Industrial
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
# Base idea: https://gitlab.com/dennevi/Board2Pdf/ (Released as Public Domain)
"""
Dependencies:
- from: RSVG
role: Create PDF, PNG, PS and EPS formats
id: rsvg1
- from: Ghostscript
role: Create PNG, PS and EPS formats
- from: ImageMagick
role: Create monochrome prints and scaled PNG files
# The plot_frame_gui() needs KiAuto to print the frame
- from: KiAuto
command: pcbnew_do
role: Print the page frame in GUI mode
version: 1.6.7
- from: LXML
role: mandatory
"""
# Direct SVG to EPS conversion is problematic.
# If we use 72 dpi the page size is ok, but some objects (currently the solder mask) have low resolution.
# So we create a PDF and then use GS to create the EPS files.
# - from: RSVG
# role: Create EPS format
# version: '2.40'
# id: rsvg2
from copy import deepcopy
import re
import os
import subprocess
import importlib
from pcbnew import B_Cu, B_Mask, F_Cu, F_Mask, FromMM, IsCopperLayer, LSET, PLOT_CONTROLLER, PLOT_FORMAT_SVG
import shlex
from shutil import rmtree
from tempfile import NamedTemporaryFile, mkdtemp
from .error import KiPlotConfigurationError
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 .kicad.config import KiConf
from .kicad.v5_sch import SchError
from .kicad.pcb import PCB
from .misc import (PDF_PCB_PRINT, W_PDMASKFAIL, W_MISSTOOL, PCBDRAW_ERR, W_PCBDRAW, VIATYPE_THROUGH, VIATYPE_BLIND_BURIED,
VIATYPE_MICROVIA)
from .create_pdf import create_pdf_from_pages
from .macros import macros, document, output_class # noqa: F401
from .drill_marks import DRILL_MARKS_MAP, add_drill_marks
from .layer import Layer, get_priority
from . import __version__
from . import log
logger = log.get_logger()
POLY_FILL_STYLE = ("fill:{0}; fill-opacity:1.0; stroke:{0}; stroke-width:1; stroke-opacity:1; stroke-linecap:round; "
"stroke-linejoin:round;fill-rule:evenodd;")
DRAWING_LAYERS = ['Dwgs.User', 'Cmts.User', 'Eco1.User', 'Eco2.User']
EXTRA_LAYERS = ['F.Fab', 'B.Fab', 'F.CrtYd', 'B.CrtYd']
# Opacity to make something invisible, but not removable
ALMOST_TRANSPARENT = '0.01'
# The following modules will be downloaded after we solve the dependencies
# They are just helpers and we solve their dependencies
svgutils = None # Will be loaded during dependency check
kicad_worksheet = None # Also needs svgutils
def pcbdraw_warnings(tag, msg):
logger.warning('{}({}) {}'.format(W_PCBDRAW, tag, msg))
def _run_command(cmd):
logger.debug('- Executing: '+shlex.join(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)
if cmd_output.strip():
logger.debug('- Output from command:\n'+cmd_output.decode())
def hex_to_rgb(value):
""" Return (red, green, blue) in float between 0-1 for the color given as #rrggbb. """
value = value.lstrip('#')
rgb = tuple(int(value[i:i+2], 16) for i in range(0, 6, 2))
rgb = (rgb[0]/255, rgb[1]/255, rgb[2]/255)
alpha = int(value[6:], 16)/255 if len(value) == 8 else 1.0
return rgb, alpha
def to_gray(color):
avg = (color[0]+color[1]+color[2])/3
return (avg, avg, avg)
def to_gray_hex(color):
rgb, alpha = hex_to_rgb(color)
avg = (rgb[0]+rgb[1]+rgb[2])/3
avg_str = '%02X' % int(avg*255)
return '#'+avg_str+avg_str+avg_str
def load_svg(file, color, colored_holes, holes_color, monochrome):
with open(file, 'rt') as f:
content = f.read()
color = color[:7]
if monochrome:
color = to_gray_hex(color)
holes_color = to_gray_hex(holes_color)
if colored_holes:
content = content.replace('#FFFFFF', '**black_hole**')
if color != '#000000':
# Files plotted
content = content.replace('#000000', color)
# Files generated by "Print"
content = content.replace('stroke:rgb(0%,0%,0%)', 'stroke:'+color)
if colored_holes:
content = content.replace('**black_hole**', holes_color)
return content
def get_size(svg):
""" Finds the width and height in viewBox units """
view_box = svg.root.get('viewBox').split(' ')
return float(view_box[2]), float(view_box[3])
class LayerOptions(Layer):
""" Data for a layer """
def __init__(self):
super().__init__()
self._unknown_is_error = True
with document:
self.color = ""
""" Color used for this layer """
self.plot_footprint_refs = True
""" Include the footprint references """
self.plot_footprint_values = True
""" Include the footprint values """
self.force_plot_invisible_refs_vals = False
""" Include references and values even when they are marked as invisible """
def config(self, parent):
super().config(parent)
if self.color:
self.validate_color('color')
def copy_extra_from(self, ref):
""" Copy members specific to LayerOptions """
self.color = ref.color
self.plot_footprint_refs = ref.plot_footprint_refs
self.plot_footprint_values = ref.plot_footprint_values
self.force_plot_invisible_refs_vals = ref.force_plot_invisible_refs_vals
class PagesOptions(Optionable):
""" One page of the output document """
def __init__(self):
super().__init__()
self._unknown_is_error = True
with document:
self.mirror = False
""" Print mirrored (X axis inverted) """
self.monochrome = False
""" Print in gray scale """
self.scaling = None
""" *[number=1.0] Scale factor (0 means autoscaling)"""
self.autoscale_margin_x = None
""" [number=0] Horizontal margin used for the autoscaling mode [mm] """
self.autoscale_margin_y = None
""" [number=0] Vertical margin used for the autoscaling mode [mm] """
self.title = ''
""" Text used to replace the sheet title. %VALUE expansions are allowed.
If it starts with `+` the text is concatenated """
self.sheet = 'Assembly'
""" Text to use for the `sheet` in the title block.
Pattern (%*) and text variables are expanded.
In addition when you use `repeat_for_layer` the following patterns are available:
%ln layer name, %ls layer suffix and %ld layer description """
self.sheet_reference_color = ''
""" Color to use for the frame and title block """
self.line_width = 0.1
""" [0.02,2] For objects without width [mm] (KiCad 5) """
self.negative_plot = False
""" Invert black and white. Only useful for a single layer """
self.exclude_pads_from_silkscreen = False
""" Do not plot the component pads in the silk screen (KiCad 5.x only) """
self.tent_vias = True
""" Cover the vias """
self.colored_holes = True
""" Change the drill holes to be colored instead of white """
self.holes_color = '#000000'
""" Color used for the holes when `colored_holes` is enabled """
self.sort_layers = False
""" *Try to sort the layers in the same order that uses KiCad for printing """
self.layers = LayerOptions
""" *[list(dict)|list(string)|string] List of layers printed in this page.
Order is important, the last goes on top.
You can reuse other layers lists, some options aren't used here, but they are valid """
self.page_id = '%02d'
""" Text to differentiate the pages. Use %d (like in C) to get the page number """
self.sketch_pads_on_fab_layers = False
""" Draw only the outline of the pads on the *.Fab layers (KiCad 6+) """
self.sketch_pad_line_width = 0.1
""" Line width for the sketched pads [mm], see `sketch_pads_on_fab_layers` (KiCad 6+)
Note that this value is currently ignored by KiCad (6.0.9) """
self.repeat_for_layer = ''
""" Use this page as a pattern to create more pages.
The other pages will change the layer mentioned here.
This can be used to generate a page for each copper layer, here you put `F.Cu`.
See `repeat_layers` """
self.repeat_layers = LayerOptions
""" [list(dict)|list(string)|string] List of layers to replace `repeat_for_layer`.
This can be used to generate a page for each copper layer, here you put `copper` """
self.repeat_inherit = True
""" If we will inherit the options of the layer we are replacing.
Disable it if you specify the options in `repeat_layers`, which is unlikely """
self._scaling_example = 1.0
self._autoscale_margin_x_example = 0
self._autoscale_margin_y_example = 0
def expand_sheet_patterns(self, parent, layer=None):
if layer:
self.sheet = self.sheet.replace('%ln', layer.layer)
self.sheet = self.sheet.replace('%ls', layer.suffix)
self.sheet = self.sheet.replace('%ld', layer.description)
self.sheet = self.expand_filename_pcb(self.sheet)
def config(self, parent):
super().config(parent)
if isinstance(self.layers, type):
raise KiPlotConfigurationError("Missing `layers` list")
# Fill the ID member for all the layers
self.layers = LayerOptions.solve(self.layers)
if self.sort_layers:
self.layers.sort(key=lambda x: get_priority(x._id), reverse=True)
if self.sheet_reference_color:
self.validate_color('sheet_reference_color')
if self.holes_color:
self.validate_color('holes_color')
if self.scaling is None:
self.scaling = parent.scaling
if self.autoscale_margin_x is None:
self.autoscale_margin_x = parent.autoscale_margin_x
if self.autoscale_margin_y is None:
self.autoscale_margin_y = parent.autoscale_margin_y
self.sketch_pad_line_width = GS.from_mm(self.sketch_pad_line_width)
# Validate the repeat_* stuff
if self.repeat_for_layer:
layer = Layer.solve(self.repeat_for_layer)
if len(layer) > 1:
raise KiPlotConfigurationError('Please specify a single layer for `repeat_for_layer`')
layer = layer[0]
self._repeat_for_layer = next(filter(lambda x: x._id == layer._id, self.layers), None)
if self._repeat_for_layer is None:
raise KiPlotConfigurationError("Layer `{}` specified in `repeat_for_layer` isn't valid".format(layer))
self._repeat_for_layer_index = self.layers.index(self._repeat_for_layer)
if isinstance(self.repeat_layers, type):
raise KiPlotConfigurationError('`repeat_for_layer` specified, but nothing to repeat')
self._repeat_layers = LayerOptions.solve(self.repeat_layers)
class PCB_PrintOptions(VariantOptions):
# Mappings to KiCad config values. They should be the same used in drill_marks.py
_pad_colors = {'pad_color': 'pad_through_hole',
'via_color': 'via_through',
'micro_via_color': 'via_micro',
'blind_via_color': 'via_blind_buried'}
def __init__(self):
with document:
self.output_name = None
""" {output} """
self.output = GS.def_global_output
""" *Filename for the output (%i=assembly, %x=pdf/ps)/(%i=assembly_page_NN, %x=svg/png/eps).
Consult the `page_number_as_extension` and `page_id` options """
self.page_number_as_extension = False
""" When enabled the %i is always `assembly`, the %x will be NN.FORMAT (i.e. 01.png).
Note: page numbers can be customized using the `page_id` option for each page """
self.hide_excluded = False
""" Hide components in the Fab layer that are marked as excluded by a variant.
Affected by global options """
self.color_theme = '_builtin_classic'
""" *Selects the color theme. Only applies to KiCad 6.
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 (worksheet, frame, etc.) """
self.sheet_reference_layout = ''
""" Worksheet file (.kicad_wks) to use. Leave empty to use the one specified in the project """
self.frame_plot_mechanism = 'internal'
""" [gui,internal,plot] Plotting the frame from Python is problematic.
This option selects a workaround strategy.
gui: uses KiCad GUI to do it. Is slow but you get the correct frame.
But it can't keep track of page numbers.
internal: KiBot loads the `.kicad_wks` and does the drawing work.
Best option, but some details are different from what the GUI generates.
plot: uses KiCad Python API. Only available for KiCad 6.
You get the default frame and some substitutions doesn't work """
self.pages = PagesOptions
""" *[list(dict)] List of pages to include in the output document.
Each page contains one or more layers of the PCB """
self.title = ''
""" Text used to replace the sheet title. %VALUE expansions are allowed.
If it starts with `+` the text is concatenated """
self.format = 'PDF'
""" *[PDF,SVG,PNG,EPS,PS] Format for the output file/s.
Note that for PS you need `ghostscript` which isn't part of the default docker images """
self.png_width = 1280
""" [0,7680] Width of the PNG in pixels. Use 0 to use as many pixels as the DPI needs for the page size """
self.dpi = 360
""" [36,1200] Resolution (Dots Per Inch) for the output file. Most objects are vectors, but thing
like the the solder mask are handled as images by the conversion tools """
self.colored_pads = True
""" Plot through-hole in a different color. Like KiCad GUI does """
self.pad_color = ''
""" Color used for `colored_pads` """
self.colored_vias = True
""" Plot vias in a different color. Like KiCad GUI does """
self.via_color = ''
""" Color used for through-hole `colored_vias` """
self.micro_via_color = ''
""" Color used for micro `colored_vias` """
self.blind_via_color = ''
""" Color used for blind/buried `colored_vias` """
self.keep_temporal_files = False
""" Store the temporal page and layer files in the output dir and don't delete them """
self.force_edge_cuts = False
""" *Add the `Edge.Cuts` to all the pages """
self.forced_edge_cuts_color = ''
""" Color used for the `force_edge_cuts` option """
self.scaling = 1.0
""" *Default scale factor (0 means autoscaling)"""
self.individual_page_scaling = True
""" Tell KiCad to apply the scaling for each page as a separated entity.
Disabling it the pages are coherent and can be superposed """
self.autoscale_margin_x = 0
""" Default horizontal margin used for the autoscaling mode [mm] """
self.autoscale_margin_y = 0
""" Default vertical margin used for the autoscaling mode [mm] """
self.realistic_solder_mask = True
""" Try to draw the solder mask as a real solder mask, not the negative used for fabrication.
In order to get a good looking select a color with transparency, i.e. '#14332440'.
PcbDraw must be installed in order to use this option """
self.add_background = False
""" Add a background to the pages, see `background_color` """
self.background_color = '#FFFFFF'
""" Color for the background when `add_background` is enabled """
self.background_image = ''
""" Background image, must be an SVG, only when `add_background` is enabled """
self.svg_precision = 4
""" [0,6] Scale factor used to represent 1 mm in the SVG (KiCad 6).
The value is how much zeros has the multiplier (1 mm = 10 power `svg_precision` units).
Note that for an A4 paper Firefox 91 and Chrome 105 can't handle more than 5 """
add_drill_marks(self)
super().__init__()
self._expand_id = 'assembly'
def config(self, parent):
super().config(parent)
if isinstance(self.pages, type):
raise KiPlotConfigurationError("Missing `pages` list")
# Expand any repeat_for_layer
pages = []
for page in self.pages:
if page.repeat_for_layer:
for la in page._repeat_layers:
new_page = deepcopy(page)
if page.repeat_inherit:
la.copy_extra_from(page._repeat_for_layer)
new_page.layers[page._repeat_for_layer_index] = la
new_page.expand_sheet_patterns(parent, la)
pages.append(new_page)
else:
page.expand_sheet_patterns(parent)
pages.append(page)
self.pages = pages
# Color theme
self._color_theme = load_color_theme(self.color_theme)
if self._color_theme is None:
raise KiPlotConfigurationError("Unable to load `{}` color theme".format(self.color_theme))
# Assign a color if none was defined
layer_id2color = self._color_theme.layer_id2color
for p in self.pages:
for la in p.layers:
if not la.color:
if la._id in layer_id2color:
la.color = layer_id2color[la._id]
else:
la.color = "#000000"
self.drill_marks = DRILL_MARKS_MAP[self.drill_marks]
self._expand_ext = self.format.lower()
for member, color in self._pad_colors.items():
if getattr(self, member):
self.validate_color(member)
else:
setattr(self, member, getattr(self._color_theme, color))
if self.forced_edge_cuts_color:
self.validate_color('forced_edge_cuts_color')
if self.frame_plot_mechanism == 'plot' and GS.ki5:
raise KiPlotConfigurationError("You can't use `plot` for `frame_plot_mechanism` with KiCad 5. It will crash.")
KiConf.init(GS.pcb_file)
if self.sheet_reference_layout:
self.sheet_reference_layout = KiConf.expand_env(self.sheet_reference_layout)
if not os.path.isfile(self.sheet_reference_layout):
raise KiPlotConfigurationError("Missing page layout file: "+self.sheet_reference_layout)
if self.add_background:
self.validate_color('background_color')
if self.background_image:
if not os.path.isfile(self.background_image):
raise KiPlotConfigurationError("Missing background image file: "+self.background_image)
with open(self.background_image, 'rt') as f:
ln = f.readline()
if not ln.startswith('<?xml') and not ln.startswith('<!DOCTYPE svg'):
raise KiPlotConfigurationError("Background image must be an SVG ({})".format(self.background_image))
def get_id_and_ext(self, n=None, id='%02d'):
try:
pn_str = id % (n+1) if n is not None else id
except TypeError:
# If id doesn't contain %d we get this exception
pn_str = id
if self.page_number_as_extension:
return self._expand_id, pn_str+'.'+self._expand_ext
return self._expand_id+'_page_'+pn_str, self._expand_ext
def get_targets(self, out_dir):
if self.format in ['SVG', 'PNG', 'EPS']:
files = []
for n, p in enumerate(self.pages):
id, ext = self.get_id_and_ext(n, p.page_id)
files.append(self.expand_filename(out_dir, self.output, id, ext))
return files
return [self._parent.expand_filename(out_dir, self.output)]
def clear_layer(self, layer):
tmp_layer = GS.board.GetLayerID(GS.work_layer)
cleared_layer = GS.board.GetLayerID(layer)
moved = []
for g in GS.board.GetDrawings():
if g.GetLayer() == cleared_layer:
g.SetLayer(tmp_layer)
moved.append(g)
for m in GS.get_modules():
for gi in m.GraphicalItems():
if gi.GetLayer() == cleared_layer:
gi.SetLayer(tmp_layer)
moved.append(gi)
self.moved_items = moved
self.cleared_layer = cleared_layer
def restore_layer(self):
for g in self.moved_items:
g.SetLayer(self.cleared_layer)
def plot_frame_api(self, pc, po, p):
""" KiCad 6 can plot the frame because it loads the worksheet format.
But not the one from the project, just a default """
self.clear_layer('Edge.Cuts')
po.SetPlotFrameRef(True)
po.SetScale(1.0)
po.SetNegative(False)
pc.SetLayer(self.cleared_layer)
pc.OpenPlotfile('frame', PLOT_FORMAT_SVG, p.sheet)
pc.PlotLayer()
pc.ClosePlot()
self.restore_layer()
def fill_kicad_vars(self, page, pages, p):
vars = {}
vars['KICAD_VERSION'] = 'KiCad E.D.A. '+GS.kicad_version+' + KiBot v'+__version__
vars['#'] = str(page)
vars['##'] = str(pages)
GS.load_pcb_title_block()
for num in range(9):
vars['COMMENT'+str(num+1)] = GS.pcb_com[num]
vars['COMPANY'] = GS.pcb_comp
vars['ISSUE_DATE'] = GS.pcb_date
vars['REVISION'] = GS.pcb_rev
# The set_title member already took care of modifying the board value
tb = GS.board.GetTitleBlock()
vars['TITLE'] = tb.GetTitle()
vars['FILENAME'] = GS.pcb_basename+'.kicad_pcb'
vars['SHEETNAME'] = p.sheet
layer = ''
for la in p.layers:
if len(layer):
layer += '+'
layer = layer+la.layer
vars['LAYER'] = layer
vars['PAPER'] = self.paper
return vars
def plot_frame_internal(self, pc, po, p, page, pages):
""" Here we plot the frame manually """
self.clear_layer('Edge.Cuts')
po.SetPlotFrameRef(False)
po.SetScale(1.0)
po.SetNegative(False)
pc.SetLayer(self.cleared_layer)
# Load the WKS
error = None
try:
ws = kicad_worksheet.Worksheet.load(self.layout)
except (kicad_worksheet.WksError, SchError) as e:
error = str(e)
if error:
raise KiPlotConfigurationError('Error reading `{}` ({})'.format(self.layout, error))
tb_vars = self.fill_kicad_vars(page, pages, p)
ws.draw(GS.board, self.cleared_layer, page, self.paper_w, self.paper_h, tb_vars)
pc.OpenPlotfile('frame', PLOT_FORMAT_SVG, p.sheet)
pc.PlotLayer()
pc.ClosePlot()
ws.undraw(GS.board)
self.restore_layer()
# We need to plot the images in a separated pass
self.last_worksheet = ws
def plot_frame_gui(self, dir_name, layer='Edge.Cuts'):
""" 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")
command = self.ensure_tool('KiAuto')
# Move all the drawings away
# KiCad 5 always prints Edge.Cuts, so we make it empty
self.clear_layer(layer)
# Start with a fresh list of files to remove
cur_files_to_remove = self._files_to_remove
self._files_to_remove = []
# Save the PCB
pcb_name, pcb_dir = self.save_tmp_dir_board('pcb_print')
self._files_to_remove.append(pcb_dir)
# Restore the layer
self.restore_layer()
# Output file name
cmd = [command, 'export', '--output_name', output, '--monochrome', '--svg', '--pads', '0',
pcb_name, dir_name, layer]
# Execute it
self.exec_with_retry(self.add_extra_options(cmd, dir_name), PDF_PCB_PRINT)
# Rotate the paper size if needed and remove the background (or it will be over the drawings)
patch_svg_file(output, remove_bkg=True, is_portrait=self.paper_portrait)
self._files_to_remove = cur_files_to_remove
def plot_pads(self, la, pc, p, filelist):
id = la._id
logger.debug('- Plotting pads for layer {} ({})'.format(la.layer, id))
# Make invisible anything but through-hole pads
tmp_layer = GS.board.GetLayerID(GS.work_layer)
moved = []
removed = []
vias = []
zones = GS.zones()
for m in GS.get_modules():
for gi in m.GraphicalItems():
if gi.GetLayer() == id:
gi.SetLayer(tmp_layer)
moved.append(gi)
for pad in m.Pads():
dr = pad.GetDrillSize()
if dr.x:
continue
layers = pad.GetLayerSet()
if GS.layers_contains(layers, id):
layers.removeLayer(id)
pad.SetLayerSet(layers)
removed.append(pad)
for e in GS.board.GetDrawings():
if e.GetLayer() == id:
e.SetLayer(tmp_layer)
moved.append(e)
for e in list(GS.board.Zones()):
layers = e.GetLayerSet()
if GS.layers_contains(layers, id):
zones.append(e)
e.UnFill()
via_type = 'VIA' if GS.ki5 else 'PCB_VIA'
for e in GS.board.GetTracks():
if e.GetClass() == via_type:
vias.append((e, e.GetDrill(), e.GetWidth()))
e.SetDrill(0)
e.SetWidth(self.min_w)
elif e.GetLayer() == id:
if e.GetWidth():
e.SetLayer(tmp_layer)
moved.append(e)
# Plot the layer
# pc.SetLayer(id) already selected
suffix = la.suffix+'_pads'
pc.OpenPlotfile(suffix, PLOT_FORMAT_SVG, p.sheet)
pc.PlotLayer()
pc.ClosePlot()
# Restore everything
for e in moved:
e.SetLayer(id)
for pad in removed:
layers = pad.GetLayerSet()
layers.addLayer(id)
pad.SetLayerSet(layers)
for (via, drill, width) in vias:
via.SetDrill(drill)
via.SetWidth(width)
if len(zones):
GS.fill_zones(GS.board, zones)
# Add it to the list
filelist.append((pc.GetPlotFileName(), self.pad_color))
def plot_vias(self, la, pc, p, filelist, via_t, via_c):
id = la._id
logger.debug('- Plotting vias for layer {} ({})'.format(la.layer, id))
# Make invisible anything but vias
tmp_layer = GS.board.GetLayerID(GS.work_layer)
moved = []
removed = []
vias = []
zones = GS.zones()
for m in GS.get_modules():
for gi in m.GraphicalItems():
if gi.GetLayer() == id:
gi.SetLayer(tmp_layer)
moved.append(gi)
for pad in m.Pads():
layers = pad.GetLayerSet()
if GS.layers_contains(layers, id):
layers.removeLayer(id)
pad.SetLayerSet(layers)
removed.append(pad)
for e in GS.board.GetDrawings():
if e.GetLayer() == id:
e.SetLayer(tmp_layer)
moved.append(e)
for e in list(GS.board.Zones()):
layers = e.GetLayerSet()
if GS.layers_contains(layers, id):
zones.append(e)
e.UnFill()
via_type = 'VIA' if GS.ki5 else 'PCB_VIA'
for e in GS.board.GetTracks():
if e.GetClass() == via_type:
if e.GetViaType() == via_t:
# Include it, but ...
if not e.IsOnLayer(id):
# This is a via that doesn't drill this layer
# Lamentably KiCad will draw a drill here
# So we create a "patch" for the hole
top = e.TopLayer()
bottom = e.BottomLayer()
w = e.GetWidth()
d = e.GetDrill()
vias.append((e, d, w, top, bottom))
e.SetWidth(d)
e.SetDrill(1)
e.SetTopLayer(F_Cu)
e.SetBottomLayer(B_Cu)
else:
top = e.TopLayer()
bottom = e.BottomLayer()
w = e.GetWidth()
d = e.GetDrill()
vias.append((e, d, w, top, bottom))
e.SetWidth(self.min_w)
elif e.GetLayer() == id:
if e.GetWidth():
e.SetLayer(tmp_layer)
moved.append(e)
# Plot the layer
suffix = la.suffix+'_vias_'+str(via_t)
pc.OpenPlotfile(suffix, PLOT_FORMAT_SVG, p.sheet)
pc.PlotLayer()
pc.ClosePlot()
# Restore everything
for e in moved:
e.SetLayer(id)
for pad in removed:
layers = pad.GetLayerSet()
layers.addLayer(id)
pad.SetLayerSet(layers)
for (via, drill, width, top, bottom) in vias:
via.SetDrill(drill)
via.SetWidth(width)
via.SetTopLayer(top)
via.SetBottomLayer(bottom)
if len(zones):
GS.fill_zones(GS.board, zones)
# Add it to the list
filelist.append((pc.GetPlotFileName(), via_c))
def add_frame_images(self, svg, monochrome):
if (not self.plot_sheet_reference or not self.frame_plot_mechanism == 'internal' or
not self.last_worksheet.has_images):
return
if monochrome:
convert_command = self.ensure_tool('ImageMagick')
for img in self.last_worksheet.images:
with NamedTemporaryFile(mode='wb', suffix='.png', delete=False) as f:
f.write(img.data)
fname = f.name
dest = fname.replace('.png', '_gray.png')
_run_command([convert_command, fname, '-set', 'colorspace', 'Gray', '-separate', '-average', dest])
with open(dest, 'rb') as f:
img.data = f.read()
os.remove(fname)
os.remove(dest)
self.last_worksheet.add_images_to_svg(svg, self.svg_precision)
def fill_polygons(self, svg, color):
""" I don't know how to generate filled polygons on KiCad 5.
So here we look for KiCad 5 unfilled polygons and transform them into filled polygons.
Note that all polygons in the frame are filled. """
logger.debug('- Filling KiCad 5 polygons')
cnt = 0
ml_coord = re.compile(r'M(\d+) (\d+) L(\d+) (\d+)')
# Scan the SVG
for e in svg.root:
if e.tag.endswith('}g'):
# This is a graphic
if len(e) < 2:
# Polygons have at least 2 paths
continue
# Check that all elements are paths and that they have the coordinates in 'd'
all_path = True
for c in e:
if not c.tag.endswith('}path') or c.get('d') is None:
all_path = False
break
if all_path:
# Ok, this is a KiCad 5 polygon
# Create a list with all the points
coords = 'M '
all_coords = True
first = True
for c in e:
coord = c.get('d')
res = ml_coord.match(coord)
if not res:
# Discard it if we can't understand the coordinates
all_coords = False
break
coords += res.group(1)+','+res.group(2)+'\n'
if first:
start = res.group(1)+','+res.group(2)
first = False
if all_coords:
# Ok, we have all the points
end = res.group(3)+','+res.group(4)
if start == end:
# Must be a closed polygon
coords += end+'\nZ'
# Make the first a single filled polygon
e[0].set('style', POLY_FILL_STYLE.format(color))
e[0].set('d', coords)
# Remove the rest
for c in e[1:]:
e.remove(c)
cnt = cnt+1
logger.debug('- Filled {} polygons'.format(cnt))
def process_background(self, svg_out, width, height):
""" Applies the background options """
if not self.add_background:
return
if self.background_image:
img = svgutils.fromfile(self.background_image)
w, h = get_size(img)
root = img.getroot()
root.moveto(0, 0)
root.scale(width/w, height/h)
svg_out.insert([root])
svg_out.insert(svgutils.RectElement(0, 0, width, height, color=self.background_color))
def fix_opacity_for_g(self, e):
contains_text = False
for c in e:
# Adjust the text opacity
if c.tag.endswith('}text'):
opacity = c.get('opacity')
if opacity is not None and opacity == '0' and c.text is not None:
c.set('opacity', ALMOST_TRANSPARENT)
c.set('style', 'font-family:monospace')
contains_text = True
elif c.tag.endswith('}g'):
# Process all text inside
self.fix_opacity_for_g(c)
# Adjust the graphic opacity
if contains_text:
style = e.get('style')
if style is not None:
e.set('style', style.replace('fill-opacity:0.0', 'fill-opacity:'+ALMOST_TRANSPARENT))
def fix_opacity(self, svg):
""" Transparent text is discarded by rsvg-convert.
So we make it almost invisible. """
logger.debug(' - Making text searchable')
# Scan the SVG
for e in svg.root:
# Look for graphics
if e.tag.endswith('}g'):
# Process all text inside
self.fix_opacity_for_g(e)
def merge_svg(self, input_folder, input_files, output_folder, output_file, p):
""" Merge all layers into one page """
first = True
for (file, color) in input_files:
logger.debug(' - Loading layer file '+file)
file = os.path.join(input_folder, file)
new_layer = svgutils.fromstring(load_svg(file, color, p.colored_holes, p.holes_color, p.monochrome))
width, height = get_size(new_layer)
# Workaround for polygon fill on KiCad 5
if GS.ki5 and file.endswith('frame.svg'):
if p.monochrome:
color = to_gray_hex(color)
self.fill_polygons(new_layer, color)
# Make text searchable
self.fix_opacity(new_layer)
if first:
svg_out = new_layer
# This is the width declared at the beginning of the file
base_width = width
first = False
self.process_background(svg_out, width, height)
self.add_frame_images(svg_out, p.monochrome)
else:
root = new_layer.getroot()
# Adjust the coordinates of this section to the main width
scale = base_width/width
if scale != 1.0:
logger.debug(' - Scaling {} by {}'.format(file, scale))
for e in root:
e.scale(scale)
svg_out.append([root])
svg_out.save(os.path.join(output_folder, output_file))
def find_paper_size(self):
pcb = PCB.load(GS.pcb_file)
self.paper_w = pcb.paper_w
self.paper_h = pcb.paper_h
self.paper_portrait = pcb.paper_portrait
self.paper = pcb.paper
def plot_extra_cu(self, id, la, pc, p, filelist):
""" Plot pads and vias to make them different """
if id >= F_Cu and id <= B_Cu:
# Here we force the same bounding box
# Problem: we will remove items, so the bbox can be affected
# Solution: we add a couple of points at the edges of the bbox
bbox = GS.board.GetBoundingBox()
track1 = GS.create_puntual_track(GS.board, bbox.GetOrigin(), id)
track2 = GS.create_puntual_track(GS.board, bbox.GetEnd(), id)
if self.colored_pads:
self.plot_pads(la, pc, p, filelist)
if self.colored_vias:
self.plot_vias(la, pc, p, filelist, VIATYPE_THROUGH, self.via_color)
self.plot_vias(la, pc, p, filelist, VIATYPE_BLIND_BURIED, self.blind_via_color)
self.plot_vias(la, pc, p, filelist, VIATYPE_MICROVIA, self.micro_via_color)
GS.board.Remove(track1)
GS.board.Remove(track2)
def pcbdraw_by_module(self, pcbdraw_file, back):
self.ensure_tool('LXML')
from .PcbDraw.plot import PcbPlotter, PlotSubstrate
# Run PcbDraw to make the heavy work (find the Edge.Cuts path and create masks)
try:
plotter = PcbPlotter(GS.board)
plotter.yield_warning = pcbdraw_warnings
plotter.render_back = back
plotter.plot_plan = [PlotSubstrate(only_mask=True)]
plotter.svg_precision = self.svg_precision
image = plotter.plot()
except (RuntimeError, SyntaxError, IOError) as e:
logger.error('PcbDraw error: '+str(e))
exit(PCBDRAW_ERR)
if GS.debug_level > 1:
# Save the SVG only for debug purposes
image.write(pcbdraw_file)
# Return the SVG as a string
from lxml.etree import tostring
return tostring(image).decode()
def plot_realistic_solder_mask(self, id, temp_dir, out_file, color, mirror, scale):
""" Plot the solder mask closer to reality, not the apertures """
if not self.realistic_solder_mask or (id != F_Mask and id != B_Mask):
return
logger.debug('- Plotting realistic solder mask using PcbDraw')
pcbdraw_file = os.path.join(temp_dir, out_file.replace('.svg', '-pcbdraw.svg'))
# Create an SVG using PcbDraw engine to generate the solder mask
svg = svgutils.fromstring(self.pcbdraw_by_module(pcbdraw_file, id == B_Mask))
# Load the plot file from KiCad to get the real coordinates system
out_file = os.path.join(temp_dir, out_file)
with open(out_file, 'rt') as f:
svg_kicad = svgutils.fromstring(f.read())
view_box = svg_kicad.root.get('viewBox')
view_box_elements = view_box.split(' ')
# This is the paper size using the SVG precision
paper_size_x = int(round(float(view_box_elements[2])))
paper_size_y = int(round(float(view_box_elements[3])))
# Compute the coordinates translation for mirror
transform = ''
if scale != 1.0 and scale:
# This is the autocenter computation used by KiCad
scale_x = scale_y = scale
board_center = GS.board.GetBoundingBox().GetCenter()
bcx, bcy = GS.iu_to_svg((board_center.x, board_center.y), self.svg_precision)
offset_x = GS.svg_round((bcx*scale-(paper_size_x/2.0))/scale)
offset_y = GS.svg_round((bcy*scale-(paper_size_y/2.0))/scale)
if mirror:
scale_x = -scale_x
offset_x += round(paper_size_x/scale)
transform = 'scale({},{}) translate({},{})'.format(scale_x, scale_y, -offset_x, -offset_y)
else:
if mirror:
transform = 'scale(-1,1) translate({},0)'.format(-paper_size_x)
# Filter the PcbDraw SVG to get what we want
defs = None
g = None
for child in svg.root:
if child.tag.endswith('}defs'):
# Keep the cut-off and pads-mask-silkscreen defs
defs = child
logger.debug(' - Found <defs>')
for df in child:
if df.get('id') not in ['cut-off', 'pads-mask-silkscreen']:
child.remove(df)
elif child.tag.endswith('}g') and child.get('id') == "boardContainer":
# Keep the solder mask
g = child
g.set('transform', transform)
g_mask = g[0]
if g_mask.get('clip-path') == "url(#cut-off)" and g_mask.get('mask') == "url(#hole-mask)":
logger.debug(' - Found clip-path')
g_mask.set('mask', "url(#pads-mask-silkscreen)")
for gf in g_mask:
if gf.get('id') != 'substrate-board':
g_mask.remove(gf)
else:
# Apply our color to the solder mask
alpha = 1.0
if len(color) == 9:
alpha = int(color[7:], 16)/255
color = color[:7]
gf.set('style', "fill:{0}; fill-opacity:{1}; stroke:{0}; stroke-width:0;".format(color, alpha))
if g is None or defs is None:
logger.warning(W_PDMASKFAIL+'Failed to extract elements from the PcbDraw SVG')
return
# Adjust the paper to what KiCad used
svg.root.set('width', svg_kicad.root.get('width'))
svg.root.set('height', svg_kicad.root.get('height'))
svg.root.set('viewBox', view_box)
# Save the filtered file
svg.save(out_file)
def set_scaling(self, po, scaling):
if scaling:
po.SetScale(scaling)
return scaling
bbox = GS.board.GetBoundingBox()
# KiCad 7 workaround, doing GS.board.GetBoundingBox().GetSize() fails
sz = bbox.GetSize()
scale_x = FromMM(self.paper_w-self.autoscale_margin_x*2)/sz.x
scale_y = FromMM(self.paper_h-self.autoscale_margin_y*2)/sz.y
scale = min(scale_x, scale_y)
po.SetScale(scale)
logger.debug('- Autoscale: {}'.format(scale))
return scale
def svg_to_pdf(self, input_folder, svg_file, pdf_file):
# Note: rsvg-convert uses 90 dpi but KiCad (and the docs I found) says SVG pt is 72 dpi
# We use a 5x scale and then reduce it to maintain the page size
# Note: rsvg 2.50.3 has this problem 2.54.5 doesn't, so we ensure the size is correct, not a fixed scale
dpi = str(self.dpi)
cmd = [self.rsvg_command, '-d', dpi, '-p', dpi, '-f', 'pdf', '-o', os.path.join(input_folder, pdf_file),
os.path.join(input_folder, svg_file)]
_run_command(cmd)
# We can't control the resolution in this way
# def svg_to_png(self, input_folder, svg_file, png_file, width):
# cmd = [self.rsvg_command, '-w', str(width), '-f', 'png', '-o', os.path.join(input_folder, png_file),
# os.path.join(input_folder, svg_file)]
# _run_command(cmd)
# Poor resolution or wrong page size, using a PDF + GS solves it
# def svg_to_eps(self, input_folder, svg_file, eps_file):
# cmd = [self.rsvg_command_eps, '-d', '72', '-p', '72', '-f', 'eps', '-o', os.path.join(input_folder, eps_file),
# os.path.join(input_folder, svg_file)]
# _run_command(cmd)
def run_gs(self, pdf_file, output, device, use_dpi=False):
cmd = [self.gs_command, '-q', '-dNOPAUSE', '-dBATCH', '-P-', '-dSAFER', '-sDEVICE='+device,
'-sOutputFile='+output, '-c', 'save', 'pop', '-f', pdf_file]
if use_dpi:
cmd.insert(1, '-r'+str(self.dpi))
_run_command(cmd)
def pdf_to_ps(self, pdf_file, output):
self.run_gs(pdf_file, output, 'ps2write')
def pdf_to_eps(self, pdf_file, output):
self.run_gs(pdf_file, output, 'eps2write')
def pdf_to_png(self, pdf_file, output):
self.run_gs(pdf_file, output, 'png16m', use_dpi=True)
if self.png_width:
# Adjust the width
convert_command = self.ensure_tool('ImageMagick')
size = str(self.png_width)+'x'
for n in range(len(self.pages)):
file = output % (n+1)
cmd = [convert_command, file, '-resize', size, file]
_run_command(cmd)
def create_pdf_from_svg_pages(self, input_folder, input_files, output_fn):
""" Convert individual SVG files into individual PDF files using 360 dpi.
Then join the individual PDF files into one PDF file scaled to the right page size. """
svg_files = []
for svg_file in input_files:
pdf_file = svg_file.replace('.svg', '.pdf')
logger.debug('- Creating {} from {}'.format(pdf_file, svg_file))
self.svg_to_pdf(input_folder, svg_file, pdf_file)
svg_files.append(os.path.join(input_folder, pdf_file))
logger.debug('- Joining {} into {} ({}x{})'.format(svg_files, output_fn, self.paper_w, self.paper_h))
create_pdf_from_pages(svg_files, output_fn, forced_width=self.paper_w)
def check_tools(self):
if self.format != 'SVG':
self.rsvg_command = self.ensure_tool('rsvg1')
self.gs_command = self.ensure_tool('Ghostscript')
# if self.format == 'EPS':
# self.rsvg_command_eps = self.ensure_tool('rsvg2')
def rename_pages(self, output_dir):
for n, p in enumerate(self.pages):
id, ext = self.get_id_and_ext(n)
cur_name = self.expand_filename(output_dir, self.output, id, ext)
id, ext = self.get_id_and_ext(n, p.page_id)
user_name = self.expand_filename(output_dir, self.output, id, ext)
if cur_name != user_name and os.path.isfile(cur_name):
os.replace(cur_name, user_name)
def generate_output(self, output):
self.check_tools()
# Avoid KiCad 5 complaining about fake vias diameter == drill == 0
self.min_w = 2 if GS.ki5 else 0
output_dir = os.path.dirname(output)
if self.keep_temporal_files:
temp_dir_base = output_dir
else:
temp_dir_base = mkdtemp(prefix='tmp-kibot-pcb_print-')
logger.debug('Starting to generate `{}`'.format(output))
logger.debug('- Temporal dir: {}'.format(temp_dir_base))
self.find_paper_size()
if self.sheet_reference_layout:
layout = self.sheet_reference_layout
else:
# Find the layout file
layout = KiConf.fix_page_layout(GS.pro_file, dry=True)[1]
if not layout or not os.path.isfile(layout):
layout = os.path.abspath(os.path.join(GS.get_resource_path('kicad_layouts'), 'default.kicad_wks'))
logger.debug('- Using layout: '+layout)
self.layout = layout
# Memorize the list of visible layers
old_visible = GS.board.GetVisibleLayers()
# Plot options
pc = PLOT_CONTROLLER(GS.board)
po = pc.GetPlotOptions()
# Set General Options:
GS.SetExcludeEdgeLayer(po, True) # We plot it separately
po.SetUseAuxOrigin(False)
po.SetAutoScale(False)
GS.SetSvgPrecision(po, self.svg_precision)
# Helpers for force_edge_cuts
if self.force_edge_cuts:
edge_layer = LayerOptions.create_layer('Edge.Cuts')
edge_id = edge_layer._id
if self.forced_edge_cuts_color:
edge_layer.color = self.forced_edge_cuts_color
else:
layer_id2color = self._color_theme.layer_id2color
if edge_id in layer_id2color:
edge_layer.color = layer_id2color[edge_id]
else:
edge_layer.color = "#000000"
# Make visible only the layers we need
# This is very important when scaling, otherwise the results are controlled by the .kicad_prl (See #407)
if not self.individual_page_scaling:
vis_layers = LSET()
for p in self.pages:
for la in p.layers:
vis_layers.addLayer(la._id)
GS.board.SetVisibleLayers(vis_layers)
# Generate the output, page by page
pages = []
for n, p in enumerate(self.pages):
# Make visible only the layers we need
# This is very important when scaling, otherwise the results are controlled by the .kicad_prl (See #407)
if self.individual_page_scaling:
vis_layers = LSET()
for la in p.layers:
vis_layers.addLayer(la._id)
GS.board.SetVisibleLayers(vis_layers)
# Use a dir for each page, avoid overwriting files, just for debug purposes
page_str = "%02d" % (n+1)
temp_dir = os.path.join(temp_dir_base, page_str)
os.makedirs(temp_dir, exist_ok=True)
po.SetOutputDirectory(temp_dir)
# Adapt the title
self.set_title(p.title if p.title else self.title)
# 1) Plot all layers to individual PDF files (B&W)
po.SetPlotFrameRef(False) # We plot it separately
po.SetMirror(p.mirror)
p.scaling = self.set_scaling(po, p.scaling)
po.SetNegative(p.negative_plot)
po.SetPlotViaOnMaskLayer(not p.tent_vias)
if GS.ki5:
po.SetLineWidth(FromMM(p.line_width))
po.SetPlotPadsOnSilkLayer(not p.exclude_pads_from_silkscreen)
else:
po.SetSketchPadsOnFabLayers(p.sketch_pads_on_fab_layers)
po.SetSketchPadLineWidth(p.sketch_pad_line_width)
filelist = []
if self.force_edge_cuts and next(filter(lambda x: x._id == edge_id, p.layers), None) is None:
p.layers.append(edge_layer)
for la in p.layers:
id = la._id
logger.debug('- Plotting layer {} ({})'.format(la.layer, id))
po.SetPlotReference(la.plot_footprint_refs)
po.SetPlotValue(la.plot_footprint_values)
po.SetPlotInvisibleText(la.force_plot_invisible_refs_vals)
# Avoid holes on non-copper layers
po.SetDrillMarksType(self.drill_marks if IsCopperLayer(id) else 0)
pc.SetLayer(id)
pc.OpenPlotfile(la.suffix, PLOT_FORMAT_SVG, p.sheet)
pc.PlotLayer()
pc.ClosePlot()
filelist.append((pc.GetPlotFileName(), la.color))
self.plot_extra_cu(id, la, pc, p, filelist)
self.plot_realistic_solder_mask(id, temp_dir, filelist[-1][0], filelist[-1][1], p.mirror, p.scaling)
# 2) Plot the frame using an empty layer and 1.0 scale
po.SetMirror(False)
if self.plot_sheet_reference:
logger.debug('- Plotting the frame')
if self.frame_plot_mechanism == 'gui':
self.plot_frame_gui(temp_dir)
elif self.frame_plot_mechanism == 'plot':
self.plot_frame_api(pc, po, p)
else: # internal
self.plot_frame_internal(pc, po, p, len(pages)+1, len(self.pages))
color = p.sheet_reference_color if p.sheet_reference_color else self._color_theme.pcb_frame
filelist.append((GS.pcb_basename+"-frame.svg", color))
# 3) Stack all layers in one file
if self.format == 'SVG':
id, ext = self.get_id_and_ext(n, p.page_id)
assembly_file = self.expand_filename(output_dir, self.output, id, ext)
else:
assembly_file = GS.pcb_basename+".svg"
logger.debug('- Merging layers to {}'.format(assembly_file))
self.merge_svg(temp_dir, filelist, temp_dir, assembly_file, p)
pages.append(os.path.join(page_str, assembly_file))
self.restore_title()
# Join all pages in one file
if self.format != 'SVG':
if self.format == 'PDF':
logger.debug('- Creating output file {}'.format(output))
self.create_pdf_from_svg_pages(temp_dir_base, pages, output)
else:
logger.debug('- Creating output files')
# PS and EPS using Ghostscript
# Create a PDF (but in a temporal place)
pdf_file = os.path.join(temp_dir, GS.pcb_basename+'_joined.pdf')
self.create_pdf_from_svg_pages(temp_dir_base, pages, pdf_file)
if self.format == 'PS':
# Use GS to create one PS
self.pdf_to_ps(pdf_file, output)
else: # EPS and PNG
id, ext = self.get_id_and_ext()
out_file = self.expand_filename(output_dir, self.output, id, ext, make_safe=False)
if self.format == 'EPS':
# Use GS to create one EPS per page
self.pdf_to_eps(pdf_file, out_file)
else:
# Use GS to create one PNG per page and then scale to the wanted width
self.pdf_to_png(pdf_file, out_file)
self.rename_pages(output_dir)
# Remove the temporal files
if not self.keep_temporal_files:
rmtree(temp_dir_base)
# Restore the list of visible layers
GS.board.SetVisibleLayers(old_visible)
logger.debug('Finished generating `{}`'.format(output))
def run(self, output):
super().run(output)
self.ensure_tool('LXML')
global svgutils
svgutils = importlib.import_module('.svgutils.transform', package=__package__)
global kicad_worksheet
kicad_worksheet = importlib.import_module('.kicad.worksheet', package=__package__)
self.filter_pcb_components()
self.generate_output(output)
self.unfilter_pcb_components()
@output_class
class PCB_Print(BaseOutput): # noqa: F821
""" PCB Print
Prints the PCB using a mechanism that is more flexible than `pdf_pcb_print` and `svg_pcb_print`.
Supports PDF, SVG, PNG, EPS and PS formats.
KiCad 5: including the frame is slow.
KiCad 6: for custom frames use the `enable_ki6_frame_fix`, is slow. """
def __init__(self):
super().__init__()
with document:
self.options = PCB_PrintOptions
""" *[dict] Options for the `pcb_print` output """
self._category = 'PCB/docs'
@staticmethod
def get_conf_examples(name, layers, templates):
outs = []
if len(DRAWING_LAYERS) < 10 and GS.ki6:
DRAWING_LAYERS.extend(['User.'+str(c+1) for c in range(9)])
extra = {la._id for la in Layer.solve(EXTRA_LAYERS)}
disabled = set()
# Check we can convert SVGs
if GS.check_tool(name, 'rsvg1') is None:
logger.warning(W_MISSTOOL+'Disabling most printed formats')
disabled |= {'PDF', 'PNG', 'EPS', 'PS'}
# Check we can convert to PS
if GS.check_tool(name, 'Ghostscript') is None:
logger.warning(W_MISSTOOL+'Disabling postscript/PDF printed format')
disabled |= {'PDF', 'PNG', 'EPS', 'PS'}
if GS.check_tool(name, 'ImageMagick') is None:
disabled |= {'PNG'}
# Generate one output for each format
for fmt in ['PDF', 'SVG', 'PNG', 'EPS', 'PS']:
if fmt in disabled:
continue
gb = {}
gb['name'] = 'basic_{}_{}'.format(name, fmt.lower())
gb['comment'] = 'PCB'
gb['type'] = name
gb['dir'] = os.path.join('PCB', fmt)
pages = []
# One page for each Cu layer
for la in layers:
page = None
mirror = False
if la.is_copper():
if la.is_top():
use_layers = ['F.Cu', 'F.Mask', 'F.Paste', 'F.SilkS', 'Edge.Cuts']
elif la.is_bottom():
use_layers = ['B.Cu', 'B.Mask', 'B.Paste', 'B.SilkS', 'Edge.Cuts']
mirror = True
else:
use_layers = [la.layer, 'Edge.Cuts']
useful = GS.get_useful_layers(use_layers+DRAWING_LAYERS, layers)
page = {}
page['layers'] = [{'layer': la.layer} for la in useful]
elif la._id in extra:
useful = GS.get_useful_layers([la, 'Edge.Cuts']+DRAWING_LAYERS, layers)
page = {}
page['layers'] = [{'layer': la.layer} for la in useful]
mirror = la.layer.startswith('B.')
if page:
if mirror:
page['mirror'] = True
if la.description:
page['sheet'] = la.description
# Change the color of the masks
for ly in page['layers']:
if ly['layer'].endswith('.Mask'):
ly['color'] = '#14332440'
pages.append(page)
ops = {'format': fmt, 'pages': pages, 'keep_temporal_files': True}
if fmt in ['PNG', 'SVG']:
ops['add_background'] = True
gb['options'] = ops
outs.append(gb)
return outs