# -*- 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) # 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 import re import os import subprocess import importlib from pcbnew import B_Cu, F_Cu, FromMM, IsCopperLayer, PLOT_CONTROLLER, PLOT_FORMAT_SVG, F_Mask, B_Mask, ZONE_FILLER 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, KICAD5_SVG_SCALE, W_MISSTOOL, PCBDRAW_ERR, W_PCBDRAW from .kiplot import exec_with_retry, add_extra_options 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() VIATYPE_THROUGH = 3 VIATYPE_BLIND_BURIED = 2 VIATYPE_MICROVIA = 1 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'] # 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._unkown_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') class PagesOptions(Optionable): """ One page of the output document """ def __init__(self): super().__init__() self._unkown_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.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 """ 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._scaling_example = 1.0 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 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.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") 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('= F_Cu and id <= B_Cu: 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) 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(view_box_elements[2]) paper_size_y = int(view_box_elements[3]) # Compute the coordinates translation for mirror transform = '' if scale != 1.0 and scale: # This the autocenter computation used by KiCad scale_x = scale_y = scale board_center = GS.board.GetBoundingBox().GetCenter() if GS.ki5: # KiCad 5 uses a different precision, we must adjust board_center.x = round(board_center.x*KICAD5_SVG_SCALE) board_center.y = round(board_center.y*KICAD5_SVG_SCALE) elif self.svg_precision != 6: # KiCad 6 can adjust the precision # The default is 6 and makes 1 KiCad unit == 1 SVG unit # But this isn't supported by browsers (Chrome and Firefox) divider = 10.0 ** (6 - self.svg_precision) board_center.x = round(board_center.x/divider) board_center.y = round(board_center.y/divider) offset_x = round((board_center.x*scale-(paper_size_x/2.0))/scale) offset_y = round((board_center.y*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 ') 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};".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 sz = GS.board.GetBoundingBox().GetSize() scale_x = FromMM(self.paper_w)/sz.x scale_y = FromMM(self.paper_h)/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.rename(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 # Plot options pc = PLOT_CONTROLLER(GS.board) po = pc.GetPlotOptions() # Set General Options: po.SetExcludeEdgeLayer(True) # We plot it separately po.SetUseAuxOrigin(False) po.SetAutoScale(False) if GS.ki6: po.SetSvgPrecision(self.svg_precision, False) # 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" # Generate the output, page by page pages = [] for n, p in enumerate(self.pages): # 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) 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) 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(GS.board) self.generate_output(output) self.unfilter_pcb_components(GS.board) @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