# -*- coding: utf-8 -*-
# Copyright (c) 2022-2025 Salvador E. Tropea
# Copyright (c) 2022-2025 Instituto Nacional de Tecnología Industrial
# License: AGPL-3.0
# Project: KiBot (formerly KiPlot)
# Base idea: https://gitlab.com/dennevi/Board2Pdf/ (Released as Public Domain)
# Drill maps contributed by Nguyen Vincent (@nguyen-v)
"""
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
from dataclasses import dataclass
import datetime
import io
import re
import os
import importlib
from pcbnew import B_Cu, B_Mask, F_Cu, F_Mask, FromMM, IsCopperLayer, LSET, PLOT_CONTROLLER, PLOT_FORMAT_SVG
from shutil import rmtree, copy2
import sys
from .error import KiPlotConfigurationError
from .fil_base import BaseFilter, apply_exclude_filter
from .gs import GS
if not GS.ki5:
    from pcbnew import PCB_GROUP, PCB_SHAPE
from .optionable import Optionable
from .out_base import VariantOptions
from .out_any_drill import DrillOptions
from .pre_base import BasePreFlight
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, FONT_HELP_TEXT, W_BUG16418, pretty_list, try_int, W_NOPAGES, W_NOLAYERS, W_NOTHREPE,
                   RENDERERS, read_png, EMBED_PREFIX, KICAD_VERSION_9_0_1)
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 .kicad.drill_info import get_num_layer_pairs, get_layer_pair_name
from .kicad.pcb_draw_helpers import draw_drill_map
from .pre_include_table import IncludeTableOptions, update_table
from .layer import Layer, get_priority
from .kiplot import run_command, load_board, get_all_components, look_for_output, get_output_targets, run_output
from .svgutils.transform import ImageElement, GroupElement
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']
GSPNERROR = re.compile(r'%\d*d[0-9a-fA-F]')
# 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


@dataclass
class ImageGroup:
    name: str
    layer: int
    bbox: tuple
    items: list


def pcbdraw_warnings(tag, msg):
    logger.warning('{}({}) {}'.format(W_PCBDRAW, tag, msg))


def _run_command(cmd):
    run_command(cmd, err_lvl=PDF_PCB_PRINT)


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.
                KiCad 6+: don't forget the alpha channel for layers like the solder mask """
            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.
                Not available on KiCad 9.0.1 and newer """
            self.use_for_center = True
            """ Use this layer for centering purposes.
                You can invert the meaning using the `invert_use_for_center` option """
            self.sketch_pads_on_fab_layers = False
            r""" Draw the outline of the pads on the \*.Fab layers (KiCad 6+).
                 When not defined we use the default value for the page """
            self.exclude_filter = Optionable
            """ [string|list(string)='_null'] Name of the filter to exclude components before printing this layer.
                This option affects only this layer.
                You should also set `plot_footprint_values` and `sketch_pads_on_fab` to false """

    def config(self, parent):
        super().config(parent)
        if self.color:
            self.validate_color('color')
        self.exclude_filter = BaseFilter.solve_filter(self.exclude_filter, 'exclude_filter')
        if not self.get_user_defined('sketch_pads_on_fab_layers'):
            self.sketch_pads_on_fab_layers = parent.sketch_pads_on_fab_layers

    @classmethod
    def solve(cls, values, parent):
        layers = super().solve(values)
        for la in layers:
            if isinstance(la.exclude_filter, type):
                la.exclude_filter = None
            if not la.get_user_defined('sketch_pads_on_fab_layers'):
                la.sketch_pads_on_fab_layers = parent.sketch_pads_on_fab_layers
        return layers

    @classmethod
    def create_layer(cls, name):
        la = super().create_layer(name)
        la.exclude_filter = None
        return la

    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
        self.use_for_center = ref.use_for_center
        self.sketch_pads_on_fab_layers = ref.sketch_pads_on_fab_layers
        self.exclude_filter = ref.exclude_filter


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.mirror_pcb_text = True
            """ Mirror text in the PCB when mirror option is enabled and we plot a user layer """
            self.mirror_footprint_text = True
            """ Mirror text in the footprints when mirror option is enabled and we plot a user layer """
            self.monochrome = False
            """ Print in gray scale """
            self.scaling = 1.0
            """ *[number=1.0] Scale factor (0 means autoscaling). When not defined we use the default value for the output """
            self.autoscale_margin_x = 0
            """ [number=0] Horizontal margin used for the autoscaling mode [mm].
                When not defined we use the default value for the output """
            self.autoscale_margin_y = 0
            """ [number=0] Vertical margin used for the autoscaling mode [mm].
                When not defined we use the default value for the output """
            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.
                The %ll is the list of layers included in this page.
                In addition when you use `repeat_for_layer` the following patterns are available:
                %ln layer name, %ls layer suffix and %ld layer description.
                When `repeat_layers` is `drill_pairs`, the following additional patterns are available:
                %lpn layer name pair, %lp layer pair """
            self.layer_var = '%ll'
            """ Text to use for the `LAYER` in the title block.
                All the expansions available for `sheet` are also available here """
            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. This option applies to KiCad 8 and older versions.
                On KiCad 9 each via can control it individually """
            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='all'] [all,selected,copper,technical,user,inners,outers,*]
                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
            r""" Draw 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='inners'] [all,selected,copper,technical,user,inners,outers,*] List
                of layers to replace `repeat_for_layer`.
                This can be used to generate a page for each copper layer, here you put `copper`.
                You can also use it to generate pages with drill maps, in this case use `drill_pairs` here.
                Note that in this case the `repeat_for_layer` should be some drawing layer, which might contain
                a group used to insert the drill table (like in the `include_table` preflight).
                The drill map needs KiCad 7 or newer """
            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
        self._layers = None
        self._layer_pair = "No layer pair"
        self._layer_pair_name = "No layer pair name"

    def __str__(self):
        txt = self.sheet
        if self._layers:
            txt += ' ['+pretty_list([la.layer for la in self._layers])+']'
        if self.scaling != 1.0:
            txt += ' auto' if not self.scaling else f' x{try_int(self.scaling)}'
        if self.mirror:
            txt += ' mirror'
        return txt

    def expand_sheet_patterns(self, parent, sheet, layers, layer=None):
        sheet = sheet.replace('%ll', layers)
        if layer:
            sheet = sheet.replace('%lpn', self._layer_pair_name)
            sheet = sheet.replace('%lp', self._layer_pair)
            sheet = sheet.replace('%ln', layer.layer)
            sheet = sheet.replace('%ls', layer.suffix)
            sheet = sheet.replace('%ld', layer.description)
        return self.expand_filename_pcb(sheet, make_safe=False)

    def config(self, parent):
        super().config(parent)
        # Fill the ID member for all the layers
        self._layers = LayerOptions.solve(self.layers, self)
        self._is_drill = False
        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 not self.get_user_defined('scaling'):
            self.scaling = parent.scaling
        if not self.get_user_defined('autoscale_margin_x'):
            self.autoscale_margin_x = parent.autoscale_margin_x
        if not self.get_user_defined('self.autoscale_margin_y'):
            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 self.repeat_layers != ['drill_pairs']:
                self._repeat_layers = LayerOptions.solve(self.repeat_layers, self)
            else:
                self._is_drill = True
                self._drill_map_layer = GS.board.GetLayerID(self.repeat_for_layer)
                if isinstance(parent.drill, bool):
                    self._drill_unify_pth_and_npth = True
                else:
                    if hasattr(parent.drill, 'unify_pth_and_npth'):  # 'auto' defaults to True
                        self._drill_unify_pth_and_npth = False if parent.drill.unify_pth_and_npth == 'no' else True
                    else:
                        self._drill_unify_pth_and_npth = True
                self._repeat_layers = (get_num_layer_pairs(self._drill_unify_pth_and_npth) *
                                       [LayerOptions.create_layer(self.repeat_for_layer)])
            if not self.repeat_layers:
                # Here we check the user specified something (or left the default)
                # We don't check this "something" is usable (self._repeat_layers) because this prevents using default values
                # for multiple projects (See #671)
                raise KiPlotConfigurationError('`repeat_for_layer` specified, but nothing to repeat')
            if not self._repeat_layers:
                # If the result is empty just note it as a warning
                logger.warning(W_NOTHREPE+'`repeat_for_layer` specified, but nothing to repeat')


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.
                Warning: you must provide a 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. Not available for KiCad 5.
                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_use_for_center = True
            """ Used when enabling the `force_edge_cuts`, in this case this is the `use_for_center` option of the forced
                layer """
            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 """
            self.invert_use_for_center = False
            """ Invert the meaning of the `use_for_center` layer option.
                This can be used to just select the edge cuts for centering, in this case enable this option
                and disable the `use_for_center` option of the edge cuts layer """
            self.include_table = IncludeTableOptions
            """ [boolean|dict=false] Use a boolean for simple cases or fine-tune its behavior.
                When enabled we include tables using the same mechanism used in the `include_table`
                preflight. The result isn't saved to disk """
            self.drill = DrillOptions
            """ [boolean|dict=false] Use a boolean for simple cases or fine-tune its behavior.
                Used to customize the `drill_pairs` option to print drill maps """
        add_drill_marks(self)
        super().__init__()
        self._expand_id = 'assembly'

    def get_layers_for_page(self, page):
        layer = ''
        for la in page._layers:
            if len(layer):
                layer += '+'
            layer = layer+la.layer
        return layer

    def config(self, parent):
        super().config(parent)
        self._include_table_output = False
        if isinstance(self.include_table, bool):
            if self.include_table:
                self._include_table_output = True
                self._include_table = IncludeTableOptions()
                self._include_table.config(self)
            else:
                self._include_table_output = False
        else:
            self._include_table_output = True
            self._include_table = self.include_table
        if isinstance(self.drill, bool):
            self._drill_unify_pth_and_npth = True
            self._drill_group_slots_and_round_holes = True
        else:
            self._drill_unify_pth_and_npth = False if self.drill.unify_pth_and_npth == 'no' else True
            self._drill_group_slots_and_round_holes = self.drill.group_slots_and_round_holes
        # Expand any repeat_for_layer
        pages = []
        for page in self.pages:
            layers_for_page = self.get_layers_for_page(page)
            if page.repeat_for_layer:
                for i, la in enumerate(page._repeat_layers):
                    new_page = deepcopy(page)
                    if page.repeat_inherit:
                        la.copy_extra_from(page._repeat_for_layer)
                    if page._is_drill:
                        new_page._drill_pair_index = i
                        new_page._layer_pair = get_layer_pair_name(new_page._drill_pair_index, False,
                                                                   self._drill_unify_pth_and_npth,
                                                                   self._drill_group_slots_and_round_holes)
                        new_page._layer_pair_name = get_layer_pair_name(new_page._drill_pair_index, True,
                                                                        self._drill_unify_pth_and_npth,
                                                                        self._drill_group_slots_and_round_holes)
                    new_page._layers[page._repeat_for_layer_index] = la
                    new_page.sheet = new_page.expand_sheet_patterns(parent, page.sheet, la.layer+'+'+layers_for_page, la)
                    new_page.layer_var = new_page.expand_sheet_patterns(parent, page.layer_var, la.layer+'+'+layers_for_page,
                                                                        la)
                    pages.append(new_page)
            else:
                page.sheet = page.expand_sheet_patterns(parent, page.sheet, layers_for_page)
                page.layer_var = page.expand_sheet_patterns(parent, page.layer_var, layers_for_page)
                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)
        self._sheet_reference_layout = self.sheet_reference_layout
        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.global_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)
        po.SetDrillMarksType(0)
        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['CURRENT_DATE'] = datetime.datetime.now().strftime('%Y-%m-%d')
        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
        vars['SHEETPATH'] = ''  # Only relevant for an schematic
        vars['LAYER'] = p.layer_var
        vars['PAPER'] = self.pcb.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)
        # We don't want drill marks in the frame
        po.SetDrillMarksType(0)
        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.pcb.paper_w, self.pcb.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')
        if self._sheet_reference_layout:
            # Worksheet override
            wks = os.path.abspath(self._sheet_reference_layout)
            KiConf.fix_page_layout(os.path.join(pcb_dir, GS.pro_fname), force_pcb=wks)
        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.pcb.paper_portrait)
        self._files_to_remove = cur_files_to_remove

    def plot_frame_ki8_external(self, dir_name, p, page, pages, color):
        logger.debug('- Plotting the frame using an external script')
        # Create a bogus PCB with the same paper size
        pcb_dir = GS.mkdtemp('pcb_print_tmp_pcb_for_frame')
        logger.debugl(1, '  - Temporal dir: '+pcb_dir)
        pcb_name = os.path.join(pcb_dir, GS.pcb_fname)
        self.pcb.write(pcb_name)
        # Copy the project
        pro_name, _, _ = GS.copy_project(pcb_name)
        # Copy the layout, user provided or default, we need to expand vars here
        # In particular KiBot internal stuff
        wks = KiConf.fix_page_layout(os.path.join(pcb_dir, GS.pro_fname), force_pcb=self.layout)
        wks = wks[1]
        logger.debugl(1, '  - Worksheet: '+wks)
        try:
            ws = kicad_worksheet.Worksheet.load(wks)
            error = None
        except (kicad_worksheet.WksError, SchError) as e:
            error = str(e)
        if error:
            raise KiPlotConfigurationError('Error reading `{}` ({})'.format(wks, error))
        # Expand the variables in the copied worksheet
        tb_vars = self.fill_kicad_vars(page, pages, p)
        ws.expand(tb_vars, remove_images=True)
        ws.save(wks)
        # Plot the frame using a helper script
        # kicad-cli fails: https://gitlab.com/kicad/code/kicad/-/issues/18928
        script = os.path.join(GS.get_resource_path('tools'), 'frame_plotter')
        c_rgb = hex_to_rgb(color)[0]
        run_command([sys.executable, script, pcb_name, str(c_rgb[0]), str(c_rgb[1]), str(c_rgb[2])])
        # Copy the result
        copy2(os.path.join(pcb_dir, GS.pcb_basename+'-frame.svg'), os.path.join(dir_name, GS.pcb_basename+"-frame.svg"))
        rmtree(pcb_dir)
        if wks and ws.has_images:
            # But ... looks like KiCad fails on images
            # https://gitlab.com/kicad/code/kicad/-/issues/18959
            logger.debugl(1, '  - Fixing images')
            # Do a manual draw, just to collect any image
            ws.draw(GS.board, GS.board.GetLayerID(GS.global_work_layer), page, self.pcb.paper_w, self.pcb.paper_h, tb_vars)
            ws.undraw(GS.board)
            # We need to plot the images in a separated pass
            self.last_worksheet = ws
        else:
            # We didn't use a loaded worksheet, external
            self.last_worksheet = False

    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.global_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(), GS.get_via_width(e)))
                e.SetDrill(0)
                GS.set_via_width(e, 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)
            GS.set_via_width(via, width)
        if len(zones):
            GS.fill_zones(GS.board, zones)
        # Add it to the list
        filelist.append((pc.GetPlotFileName(), self.pad_color))
        return len(zones)

    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.global_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 = GS.get_via_width(e)
                        d = e.GetDrill()
                        vias.append((e, d, w, top, bottom))
                        GS.set_via_width(e, d)
                        e.SetDrill(1)
                        e.SetTopLayer(F_Cu)
                        e.SetBottomLayer(B_Cu)
                else:
                    top = e.TopLayer()
                    bottom = e.BottomLayer()
                    w = GS.get_via_width(e)
                    d = e.GetDrill()
                    vias.append((e, d, w, top, bottom))
                    GS.set_via_width(e, 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)
            GS.set_via_width(via, 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))
        return len(zones)

    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 or not self.last_worksheet.has_images):
            return
        if monochrome:
            convert_command = self.ensure_tool('ImageMagick')
            for img in self.last_worksheet.images:
                fname = GS.tmp_file(content=img.data, suffix='.png', binary=True)
                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 search_text_for_g(self, e, texts):
        transform = e.get('transform')
        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:
                    cp_text = svgutils.TextElement(c.get('x'), c.get('y'), c.text, font='monospace',
                                                   color=self.background_color, anchor=c.get('text-anchor'),
                                                   size=c.get('font-size'), lengthAdjust=c.get('lengthAdjust'),
                                                   textLength=c.get('textLength'))
                    if transform is None:
                        texts.append(cp_text)
                    else:
                        # Rotated text
                        texts.append(svgutils.GroupElement([cp_text], {'transform': transform}))
            elif c.tag.endswith('}g'):
                # Process all text inside
                self.search_text_for_g(c, texts)

    def search_text(self, svg, texts):
        """ Transparent text is discarded by rsvg-convert """
        logger.debug(' - Looking for text tags')
        # Scan the SVG
        for e in svg.root:
            # Look for graphics
            if e.tag.endswith('}g'):
                # Process all text inside
                self.search_text_for_g(e, texts)

    def add_drill_map_drawing(self, p, g):
        if p._is_drill:
            if not GS.ki7:
                raise KiPlotConfigurationError('The `pcb_print` drill map needs KiCad 7 or newer')
            layer = p._drill_map_layer
            index = p._drill_pair_index
            draw_drill_map(g, layer, index, self._drill_unify_pth_and_npth,
                           self._drill_group_slots_and_round_holes)

    def move_kibot_image_groups(self):
        """ Look for KiBot image groups (kibot_image_*)
            Move them to the Rescue layer
            Memorize them to restore and analysis """
        self._image_groups = []
        if GS.ki5:
            # Groups were introduced in KiCad 6
            return
        tmp_layer = GS.board.GetLayerID(GS.global_work_layer)
        for g in GS.board.Groups():
            name = g.GetName()
            if not name.startswith('kibot_image_'):
                continue
            x1, y1, x2, y2 = GS.compute_group_boundary(g)
            moved = []
            layer = None
            for item in g.GetItems():
                if layer is None:
                    layer = item.GetLayer()
                moved.append((item, item.GetLayer()))
                item.SetLayer(tmp_layer)
            self._image_groups.append(ImageGroup(name, layer, (x1, y1, x2, y2), moved))

    def restore_kibot_image_groups(self):
        """ Move the KiBot image groups (kibot_image_*) to their original layers """
        for g in self._image_groups:
            for item in g.items:
                item[0].SetLayer(item[1])
        self._image_groups = []

    def add_output_images(self, svg, page):
        """ Look for groups named kibot_image_OUTPUT and paste images from the referred OUTPUTs """
        # Check which layers we printed
        layers = {la._id for la in page._layers}
        # Look for groups
        logger.debug('Looking for image groups in the PCB')
        for g in self._image_groups:
            name = g.name
            if not name.startswith('kibot_image_'):
                continue
            logger.debugl(2, f'- Found {name}')
            # Check if this group is for a layer we printed
            if g.layer not in layers:
                logger.debug('- {name} not in printed layers')
                continue
            # Look for the image from the output
            output_name = name[12:]
            output_obj = look_for_output(output_name, '`include image`', self._parent, RENDERERS)
            targets, _, _ = get_output_targets(output_name, self._parent)
            targets = [fn for fn in targets if fn.endswith('.png')]
            if not targets:
                raise KiPlotConfigurationError("PCB group `{name}` uses `{output_name}` which doesn't generate any PNG")
            fname = targets[0]
            logger.debugl(2, f'- Related image: {fname}')
            if not os.path.exists(fname):
                # The target doesn't exist
                if not output_obj._done:
                    # The output wasn't created in this run, try running it
                    logger.debug('- Not yet generated, tying to generate it')
                    run_output(output_obj)
            if not os.path.exists(fname):
                raise KiPlotConfigurationError("Failed to generate `{fname}` for PCB group `{name}`")
            # Add the image to the SVG
            try:
                s, w, h, dpi = read_png(fname, logger, only_size=False)
            except TypeError as e:
                raise KiPlotConfigurationError(f'Error reading {fname} size: {e} for PCB group `{name}`')
            logger.debugl(2, f'- PNG: {w}x{h} {dpi} PPIs')
            x1, y1, x2, y2 = g.bbox
            logger.debugl(2, f'- Box: {x1},{y1} {x2},{y2} IUs')
            # Convert pixels to mm and then to KiCad units
            # w = GS.from_mm(w/dpi*25.4)
            # h = GS.from_mm(h/dpi*25.4)
            # logger.error(f'{w}x{h} IUs')
            scale = GS.iu_to_svg(1.0, self.svg_precision)  # This is the scale to convert IUs to SVG units
            logger.debugl(2, f'- Scale {scale}')
            #  Put the image at the box coordinates with its size
            img = ImageElement(io.BytesIO(s), x2-x1, y2-y1)
            img.moveto(x1, y1)
            img.scale(scale)
            #  Put the image in a group
            g = GroupElement([img])
            #  Add the group to the SVG
            svg.append(g)

    def merge_svg(self, input_folder, input_files, output_folder, output_file, p):
        """ Merge all layers into one page """
        first = True
        texts = []
        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)
            if self.format == 'PDF':
                # Look for transparent text that we will copy to a suitable place
                self.search_text(new_layer, texts)
            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)
                self.add_output_images(svg_out, p)
            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])
        if self.format == 'PDF':
            # Make the text searchable
            # Add it before anything using the background color
            for text in texts:
                svg_out.insert(text)
        svg_out.save(os.path.join(output_folder, output_file))

    def plot_extra_cu(self, id, la, pc, p, filelist):
        """ Plot pads and vias to make them different """
        re_filled_zones = False
        if IsCopperLayer(id):
            # 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:
                re_filled_zones |= self.plot_pads(la, pc, p, filelist)
            if self.colored_vias:
                re_filled_zones |= self.plot_vias(la, pc, p, filelist, VIATYPE_THROUGH, self.via_color)
                re_filled_zones |= self.plot_vias(la, pc, p, filelist, VIATYPE_BLIND_BURIED, self.blind_via_color)
                re_filled_zones |= self.plot_vias(la, pc, p, filelist, VIATYPE_MICROVIA, self.micro_via_color)

            GS.board.Remove(track1)
            GS.board.Remove(track2)
        return re_filled_zones

    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:
            GS.exit_with_error('PcbDraw error: '+str(e), 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 kicad7_scale_workaround(self, id, temp_dir, out_file, color, mirror, scale):
#         logger.error(f'Fix {out_file}')
#         svg = svgutils.fromfile(out_file)
#         logger.error(self.get_view_box(svg))
#         bbox = GS.board.GetBoundingBox()
#         logger.error(bbox.GetSize())
#         for f in GS.board.Footprints():
#             logger.error('Pads')
#             for p in f.Pads():
#                 bbox = p.GetBoundingBox()
#                 logger.error(bbox.GetSize())
#             logger.error('Graphics')
#             for gi in f.GraphicalItems():
#                 bbox = gi.GetBoundingBox()
#                 logger.error(bbox.GetSize())
#         logger.error('Drawings')
#         for d in GS.board.Drawings():
#             bbox = d.GetBoundingBox()
#             logger.error(bbox.GetSize())
#         logger.error('Tracks')
#         for t in GS.board.Tracks():
#             bbox = t.GetBoundingBox()
#             logger.error(bbox.GetSize())
#
#     def get_view_box(self, svg):
#         return tuple(map(lambda x: float(x), svg.root.get('viewBox').split(' ')))

    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 = float(view_box_elements[2])
        paper_size_y = 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 += 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)  # noqa: B038
            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)  # noqa: B038
                        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.pcb.paper_w-self.autoscale_margin_x*2)/sz.x if sz.x else 1
        scale_y = FromMM(self.pcb.paper_h-self.autoscale_margin_y*2)/sz.y if sz.y else 1
        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.pcb.paper_w, self.pcb.paper_h))
        create_pdf_from_pages(svg_files, output_fn, forced_width=self.pcb.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, real_output=None):
        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, real_output or self.output, id, ext)
            if cur_name != user_name and os.path.isfile(cur_name):
                logger.debug('- Renaming {cur_name} -> {user_name}')
                os.replace(cur_name, user_name)
        if real_output:
            # Revert the output, GS workaround
            self.output = real_output

    def check_ki7_scale_issue(self):
        """ Check if all visible layers has scaling problems """
        if not GS.ki7:
            return False
        cur_vis = GS.board.GetVisibleLayers()
        # Copper layers are OK
        if len(cur_vis.CuStack()):
            return False
        for la in ['Edge.Cuts', 'F.SilkS', 'B.SilkS']:
            if cur_vis.Contains(Layer.DEFAULT_LAYER_NAMES[la]):
                return False
        return True

    def mirror_text(self, page, id):
        """ Mirror text in the user layers """
        if not page.mirror:
            return
        extra_debug = GS.debug_level > 2
        if extra_debug:
            logger.debug('mirror_text processing')
        if page.mirror_pcb_text:
            for g in GS.board.GetDrawings():
                if g.GetLayer() == id:
                    if hasattr(g, 'GetShownText'):
                        if extra_debug:
                            logger.debug(f'- {g.GetClass()} {GS.get_shown_text(g)} @ {g.GetCenter()}'
                                         f' mirrored: {g.IsMirrored()} just: {g.GetHorizJustify()}')
                        g.SetMirrored(not g.IsMirrored())
                        g.SetHorizJustify(-g.GetHorizJustify())
        if page.mirror_footprint_text:
            for m in GS.get_modules():
                for g in m.GraphicalItems():
                    if g.GetLayer() == id:
                        if hasattr(g, 'GetShownText'):
                            if extra_debug:
                                logger.debug(f'- {g.GetClass()} {GS.get_shown_text(g)} @ {g.GetCenter()}'
                                             f' mirrored: {g.IsMirrored()} just: {g.GetHorizJustify()}')
                            g.SetMirrored(not g.IsMirrored())
                            g.SetHorizJustify(-g.GetHorizJustify())

    def init_plot_controller(self):
        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)
        return pc, po

    def set_visible(self, edge_id):
        if not self.individual_page_scaling:
            # Make all the layers in all the pages visible
            vis_layers = LSET()
            for p in self._pages:
                for la in p._layers:
                    if la.use_for_center ^ self.invert_use_for_center:
                        vis_layers.addLayer(la._id)
            if self.force_edge_cuts and (self.forced_edge_cuts_use_for_center ^ self.invert_use_for_center):
                vis_layers.addLayer(edge_id)
            GS.board.SetVisibleLayers(vis_layers)

    def exclude_components_from_layer(self, layer):
        if not layer.exclude_filter:
            return
        comps = self._comps_for_fab_ex = self._comps if self._comps else get_all_components()
        comps_hash = self._comps_hash_for_fab_ex = {c.ref: c for c in comps}
        self._old_included_fab = [c.included for c in comps]
        apply_exclude_filter(comps, layer.exclude_filter)
        # Save the current savings
        self._layer_id_ex, self._graphs_ex = self.remove_graphics_from_layer(GS.board, comps_hash, layer.layer)

    def restore_components_from_layer(self, layer):
        if not layer.exclude_filter:
            return
        for c, v in zip(self._comps_for_fab_ex, self._old_included_fab):
            c.included = v
        self.restore_graphics_from_layer(GS.board, self._comps_hash_for_fab_ex, self._layer_id_ex, self._graphs_ex)

    def generate_output(self, output):
        self.check_tools()
        if not self._pages:
            logger.warning(W_NOPAGES+f'No pages to include in `{self._parent.name}`')
        # 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)
        temp_dir_base = output_dir if self.keep_temporal_files else GS.mkdtemp('pcb_print')
        logger.debug(f'Starting to generate `{output}`')
        logger.debug(f'- Temporal dir: {temp_dir_base}')
        # Find information about the page (size, orientation)
        self.pcb = PCB.load(GS.pcb_file)
        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 layout.startswith(EMBED_PREFIX) and 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, po = self.init_plot_controller()
        # Helpers for force_edge_cuts
        edge_id = None
        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)
        self.set_visible(edge_id)
        # Move KiBot image groups away
        self.move_kibot_image_groups()

        # Update all tables
        if GS.ki7 and self._include_table_output:
            update_table(self._include_table, self)

        # Generate the output, page by page
        pages = []
        for n, p in enumerate(self._pages):
            if not GS.ki5:
                if p._is_drill:
                    g_drill_map = PCB_GROUP(GS.board)
                    self.add_drill_map_drawing(p, g_drill_map)
                    if GS.ki7 and self._include_table_output:
                        update_table(self._include_table, self, p._drill_pair_index, True)
            # 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:
                    if la.use_for_center ^ self.invert_use_for_center:
                        vis_layers.addLayer(la._id)
                if self.force_edge_cuts and (self.forced_edge_cuts_use_for_center ^ self.invert_use_for_center):
                    vis_layers.addLayer(edge_id)
                GS.board.SetVisibleLayers(vis_layers)
            needs_ki7_scale_workaround = p.scaling != 1.0 and self.check_ki7_scale_issue()
            if needs_ki7_scale_workaround:
                logger.warning(f"{W_BUG16418}In output `{self._parent.name}` page {n+1}: "
                               "KiCad 7 bug #16418 prevents correct page view. "
                               "Add some copper, silk or edge layer")
            # 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)
            if not GS.ki9:
                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.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)
            user_layer_ids = set(Layer._get_user().values())
            if p.layers == ['all'] and not p.get_user_defined('layers'):
                logger.warning(W_NOLAYERS+f'No layers specified for `{p}` (`{self._parent.name}`), including `all`')
            re_filled_zones = False
            for pos, la in enumerate(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)
                if GS.kicad_version_n < KICAD_VERSION_9_0_1:
                    po.SetPlotInvisibleText(la.force_plot_invisible_refs_vals)
                if GS.ki6:
                    po.SetSketchPadsOnFabLayers(la.sketch_pads_on_fab_layers)
                # Avoid holes on non-copper layers
                po.SetDrillMarksType(self._drill_marks if IsCopperLayer(id) else 0)
                pc.SetLayer(id)
                if id in user_layer_ids:
                    self.mirror_text(p, id)
                # Apply a filter to this layer
                self.exclude_components_from_layer(la)
                pc.OpenPlotfile(la.suffix+str(pos), PLOT_FORMAT_SVG, p.sheet)
                pc.PlotLayer()
                if id in user_layer_ids:
                    self.mirror_text(p, id)
                pc.ClosePlot()
                filelist.append((pc.GetPlotFileName(), la.color))
                re_filled_zones |= 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)
                # Revert a filter to this layer
                self.restore_components_from_layer(la)
#                 if needs_ki7_scale_workaround:
#                     self.kicad7_scale_workaround(id, temp_dir, filelist[-1][0], filelist[-1][1], p.mirror, p.scaling)
            # remove the drill map drawing
            if not GS.ki5:
                if p._is_drill:
                    items = g_drill_map.GetItems()
                    if not isinstance(items, list):
                        items = list[items]
                    for item in items:
                        if isinstance(item, PCB_SHAPE):
                            GS.board.Delete(item)
            # 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')
                color = p.sheet_reference_color if p.sheet_reference_color else self._color_theme.pcb_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
                    if GS.ki8 and GS.pro_file:
                        # KiCad 8 adds colors and custom fonts, the API doesn't support them
                        # So we use a trick to plot the frame externally
                        self.plot_frame_ki8_external(temp_dir, p, len(pages)+1, len(self._pages), color)
                        # We already have the correct color, marking it as black will keep the color
                        color = '#000000'
                    else:
                        self.plot_frame_internal(pc, po, p, len(pages)+1, len(self._pages))
                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()
            # If we forced a refill and the user doesn't want it just reload the PCB
            if re_filled_zones and not BasePreFlight.get_option('check_zone_fills'):
                fill_pre = BasePreFlight.get_preflight('fill_zones')
                if not fill_pre or not fill_pre._enabled:
                    self.unfilter_pcb_components()
                    load_board(forced=True)
                    self.filter_pcb_components()
                    # Plot options
                    pc, po = self.init_plot_controller()
                    # Make visible only the layers we need
                    self.set_visible(edge_id)

        # 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)
                    real_output = None
                    # Ghostscript issue workaround
                    # Patterns like %02d followed by an hex digit makes GS fail (i.e. GS 10.00.0)
                    # So here we use a pattern that works and then we rename the files
                    if GSPNERROR.search(out_file):
                        real_output = self.output
                        self.output = '_kibot_tmp_%i.'+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, real_output)
        # Restore KiBot image groups away
        self.restore_kibot_image_groups()
        # 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.
        You can add images generated other outputs to your print, see :ref:`add_print_images`.
        Important: `colored_vias` and `colored_pads` usually involves a zones refill. To avoid side
        effects we reload the PCB, which might be slow. If refills are tolerable for your case and
        you want to make it faster just add a `check_zone_fills` preflight. This will skip the reload """
    __doc__ += FONT_HELP_TEXT

    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):
        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)
            # Drill map
            if GS.ki7:
                page = {'repeat_for_layer': 'User.Drawings', 'repeat_layers': 'drill_pairs', 'layers':
                        [{'layer': 'User.Drawings', 'color': '#000000'}, {'layer': 'Edge.Cuts', 'color': '#000000'}]}
                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
