Просмотр исходного кода

- made FlatCAMExcellon and FlatCAMGerber into their own files in the flatcamParser folder

Marius Stanciu 6 лет назад
Родитель
Сommit
8762b115c9
7 измененных файлов с 3418 добавлено и 3404 удалено
  1. 2 0
      FlatCAMObj.py
  2. 1 0
      README.md
  3. 4 3404
      camlib.py
  4. 1 0
      flatcamEditors/FlatCAMExcEditor.py
  5. 1 0
      flatcamEditors/FlatCAMGrbEditor.py
  6. 1433 0
      flatcamParsers/ParseExcellon.py
  7. 1976 0
      flatcamParsers/ParseGerber.py

+ 2 - 0
FlatCAMObj.py

@@ -16,6 +16,8 @@ from flatcamGUI.ObjectUI import *
 from FlatCAMCommon import LoudDict
 from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 from camlib import *
+from flatcamParsers.ParseExcellon import Excellon
+from flatcamParsers.ParseGerber import Gerber
 
 import itertools
 import tkinter as tk

+ 1 - 0
README.md

@@ -15,6 +15,7 @@ CAD program, and create G-Code for Isolation routing.
 - working in adding to the Optimal Tool the rest of the distances found in the Gerber and the locations associated; added GUI
 - added display of the results for the Rules Check Tool in a formatted way
 - made the Rules Check Tool document window Read Only
+- made FlatCAMExcellon and FlatCAMGerber into their own files in the flatcamParser folder
 
 5.10.2019
 

+ 4 - 3404
camlib.py

@@ -25,7 +25,6 @@ from rtree import index as rtindex
 from lxml import etree as ET
 
 # See: http://toblerity.org/shapely/manual.html
-
 from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString
 from shapely.geometry import MultiPoint, MultiPolygon
 from shapely.geometry import box as shply_box
@@ -54,15 +53,16 @@ import ezdxf
 from flatcamParsers.ParseSVG import *
 from flatcamParsers.ParseDXF import *
 
+if platform.architecture()[0] == '64bit':
+    from ortools.constraint_solver import pywrapcp
+    from ortools.constraint_solver import routing_enums_pb2
+
 import logging
 import FlatCAMApp
 import gettext
 import FlatCAMTranslation as fcTranslate
 import builtins
 
-if platform.architecture()[0] == '64bit':
-    from ortools.constraint_solver import pywrapcp
-    from ortools.constraint_solver import routing_enums_pb2
 
 fcTranslate.apply_language('strings')
 
@@ -2075,3406 +2075,6 @@ class ApertureMacro:
         return self.geometry
 
 
-class Gerber (Geometry):
-    """
-    Here it is done all the Gerber parsing.
-
-    **ATTRIBUTES**
-
-    * ``apertures`` (dict): The keys are names/identifiers of each aperture.
-      The values are dictionaries key/value pairs which describe the aperture. The
-      type key is always present and the rest depend on the key:
-
-    +-----------+-----------------------------------+
-    | Key       | Value                             |
-    +===========+===================================+
-    | type      | (str) "C", "R", "O", "P", or "AP" |
-    +-----------+-----------------------------------+
-    | others    | Depend on ``type``                |
-    +-----------+-----------------------------------+
-    | solid_geometry      | (list)                  |
-    +-----------+-----------------------------------+
-    * ``aperture_macros`` (dictionary): Are predefined geometrical structures
-      that can be instantiated with different parameters in an aperture
-      definition. See ``apertures`` above. The key is the name of the macro,
-      and the macro itself, the value, is a ``Aperture_Macro`` object.
-
-    * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
-      from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
-
-    * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
-      *buffering* (or thickening) the ``paths`` with the aperture. These are
-      generated from ``paths`` in ``buffer_paths()``.
-
-    **USAGE**::
-
-        g = Gerber()
-        g.parse_file(filename)
-        g.create_geometry()
-        do_something(s.solid_geometry)
-
-    """
-
-    # defaults = {
-    #     "steps_per_circle": 128,
-    #     "use_buffer_for_union": True
-    # }
-
-    def __init__(self, steps_per_circle=None):
-        """
-        The constructor takes no parameters. Use ``gerber.parse_files()``
-        or ``gerber.parse_lines()`` to populate the object from Gerber source.
-
-        :return: Gerber object
-        :rtype: Gerber
-        """
-
-        # How to approximate a circle with lines.
-        self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"])
-
-        # Initialize parent
-        Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["gerber_circle_steps"]))
-
-        # Number format
-        self.int_digits = 3
-        """Number of integer digits in Gerber numbers. Used during parsing."""
-
-        self.frac_digits = 4
-        """Number of fraction digits in Gerber numbers. Used during parsing."""
-
-        self.gerber_zeros = self.app.defaults['gerber_def_zeros']
-        """Zeros in Gerber numbers. If 'L' then remove leading zeros, if 'T' remove trailing zeros. Used during parsing.
-        """
-
-        # ## Gerber elements # ##
-        '''
-        apertures = {
-            'id':{
-                'type':string, 
-                'size':float, 
-                'width':float,
-                'height':float,
-                'geometry': [],
-            }
-        }
-        apertures['geometry'] list elements are dicts
-        dict = {
-            'solid': [],
-            'follow': [],
-            'clear': []
-        }
-        '''
-
-        # store the file units here:
-        self.gerber_units = self.app.defaults['gerber_def_units']
-
-        # aperture storage
-        self.apertures = {}
-
-        # Aperture Macros
-        self.aperture_macros = {}
-
-        # will store the Gerber geometry's as solids
-        self.solid_geometry = Polygon()
-
-        # will store the Gerber geometry's as paths
-        self.follow_geometry = []
-
-        # made True when the LPC command is encountered in Gerber parsing
-        # it allows adding data into the clear_geometry key of the self.apertures[aperture] dict
-        self.is_lpc = False
-
-        self.source_file = ''
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from Geometry.
-        self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
-                           'aperture_macros', 'solid_geometry', 'source_file']
-
-        # ### Parser patterns ## ##
-        # FS - Format Specification
-        # The format of X and Y must be the same!
-        # L-omit leading zeros, T-omit trailing zeros, D-no zero supression
-        # A-absolute notation, I-incremental notation
-        self.fmt_re = re.compile(r'%?FS([LTD])?([AI])X(\d)(\d)Y\d\d\*%?$')
-        self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
-        self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$')
-
-        # Mode (IN/MM)
-        self.mode_re = re.compile(r'^%?MO(IN|MM)\*%?$')
-
-        # Comment G04|G4
-        self.comm_re = re.compile(r'^G0?4(.*)$')
-
-        # AD - Aperture definition
-        # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+}
-        # NOTE: Adding "-" to support output from Upverter.
-        self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$')
-
-        # AM - Aperture Macro
-        # Beginning of macro (Ends with *%):
-        # self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
-
-        # Tool change
-        # May begin with G54 but that is deprecated
-        self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
-
-        # G01... - Linear interpolation plus flashes with coordinates
-        # Operation code (D0x) missing is deprecated... oh well I will support it.
-        self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
-
-        # Operation code alone, usually just D03 (Flash)
-        self.opcode_re = re.compile(r'^D0?([123])\*$')
-
-        # G02/3... - Circular interpolation with coordinates
-        # 2-clockwise, 3-counterclockwise
-        # Operation code (D0x) missing is deprecated... oh well I will support it.
-        # Optional start with G02 or G03, optional end with D01 or D02 with
-        # optional coordinates but at least one in any order.
-        self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))' +
-                                  '?(?=.*I([\+-]?\d+))?(?=.*J([\+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
-
-        # G01/2/3 Occurring without coordinates
-        self.interp_re = re.compile(r'^(?:G0?([123]))\*')
-
-        # Single G74 or multi G75 quadrant for circular interpolation
-        self.quad_re = re.compile(r'^G7([45]).*\*$')
-
-        # Region mode on
-        # In region mode, D01 starts a region
-        # and D02 ends it. A new region can be started again
-        # with D01. All contours must be closed before
-        # D02 or G37.
-        self.regionon_re = re.compile(r'^G36\*$')
-
-        # Region mode off
-        # Will end a region and come off region mode.
-        # All contours must be closed before D02 or G37.
-        self.regionoff_re = re.compile(r'^G37\*$')
-
-        # End of file
-        self.eof_re = re.compile(r'^M02\*')
-
-        # IP - Image polarity
-        self.pol_re = re.compile(r'^%?IP(POS|NEG)\*%?$')
-
-        # LP - Level polarity
-        self.lpol_re = re.compile(r'^%LP([DC])\*%$')
-
-        # Units (OBSOLETE)
-        self.units_re = re.compile(r'^G7([01])\*$')
-
-        # Absolute/Relative G90/1 (OBSOLETE)
-        self.absrel_re = re.compile(r'^G9([01])\*$')
-
-        # Aperture macros
-        self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
-        self.am2_re = re.compile(r'(.*)%$')
-
-        self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"]
-
-    def aperture_parse(self, apertureId, apertureType, apParameters):
-        """
-        Parse gerber aperture definition into dictionary of apertures.
-        The following kinds and their attributes are supported:
-
-        * *Circular (C)*: size (float)
-        * *Rectangle (R)*: width (float), height (float)
-        * *Obround (O)*: width (float), height (float).
-        * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
-        * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
-
-        :param apertureId: Id of the aperture being defined.
-        :param apertureType: Type of the aperture.
-        :param apParameters: Parameters of the aperture.
-        :type apertureId: str
-        :type apertureType: str
-        :type apParameters: str
-        :return: Identifier of the aperture.
-        :rtype: str
-        """
-        if self.app.abort_flag:
-            # graceful abort requested by the user
-            raise FlatCAMApp.GracefulException
-
-        # Found some Gerber with a leading zero in the aperture id and the
-        # referenced it without the zero, so this is a hack to handle that.
-        apid = str(int(apertureId))
-
-        try:  # Could be empty for aperture macros
-            paramList = apParameters.split('X')
-        except:
-            paramList = None
-
-        if apertureType == "C":  # Circle, example: %ADD11C,0.1*%
-            self.apertures[apid] = {"type": "C",
-                                    "size": float(paramList[0])}
-            return apid
-        
-        if apertureType == "R":  # Rectangle, example: %ADD15R,0.05X0.12*%
-            self.apertures[apid] = {"type": "R",
-                                    "width": float(paramList[0]),
-                                    "height": float(paramList[1]),
-                                    "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)}  # Hack
-            return apid
-
-        if apertureType == "O":  # Obround
-            self.apertures[apid] = {"type": "O",
-                                    "width": float(paramList[0]),
-                                    "height": float(paramList[1]),
-                                    "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)}  # Hack
-            return apid
-        
-        if apertureType == "P":  # Polygon (regular)
-            self.apertures[apid] = {"type": "P",
-                                    "diam": float(paramList[0]),
-                                    "nVertices": int(paramList[1]),
-                                    "size": float(paramList[0])}  # Hack
-            if len(paramList) >= 3:
-                self.apertures[apid]["rotation"] = float(paramList[2])
-            return apid
-
-        if apertureType in self.aperture_macros:
-            self.apertures[apid] = {"type": "AM",
-                                    "macro": self.aperture_macros[apertureType],
-                                    "modifiers": paramList}
-            return apid
-
-        log.warning("Aperture not implemented: %s" % str(apertureType))
-        return None
-        
-    def parse_file(self, filename, follow=False):
-        """
-        Calls Gerber.parse_lines() with generator of lines
-        read from the given file. Will split the lines if multiple
-        statements are found in a single original line.
-
-        The following line is split into two::
-
-            G54D11*G36*
-
-        First is ``G54D11*`` and seconds is ``G36*``.
-
-        :param filename: Gerber file to parse.
-        :type filename: str
-        :param follow: If true, will not create polygons, just lines
-            following the gerber path.
-        :type follow: bool
-        :return: None
-        """
-
-        with open(filename, 'r') as gfile:
-
-            def line_generator():
-                for line in gfile:
-                    line = line.strip(' \r\n')
-                    while len(line) > 0:
-
-                        # If ends with '%' leave as is.
-                        if line[-1] == '%':
-                            yield line
-                            break
-
-                        # Split after '*' if any.
-                        starpos = line.find('*')
-                        if starpos > -1:
-                            cleanline = line[:starpos + 1]
-                            yield cleanline
-                            line = line[starpos + 1:]
-
-                        # Otherwise leave as is.
-                        else:
-                            # yield clean line
-                            yield line
-                            break
-
-            processed_lines = list(line_generator())
-            self.parse_lines(processed_lines)
-
-    # @profile
-    def parse_lines(self, glines):
-        """
-        Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
-        ``self.flashes``, ``self.regions`` and ``self.units``.
-
-        :param glines: Gerber code as list of strings, each element being
-            one line of the source file.
-        :type glines: list
-        :return: None
-        :rtype: None
-        """
-
-        # Coordinates of the current path, each is [x, y]
-        path = []
-
-        # this is for temporary storage of solid geometry until it is added to poly_buffer
-        geo_s = None
-
-        # this is for temporary storage of follow geometry until it is added to follow_buffer
-        geo_f = None
-
-        # Polygons are stored here until there is a change in polarity.
-        # Only then they are combined via cascaded_union and added or
-        # subtracted from solid_geometry. This is ~100 times faster than
-        # applying a union for every new polygon.
-        poly_buffer = []
-
-        # store here the follow geometry
-        follow_buffer = []
-
-        last_path_aperture = None
-        current_aperture = None
-
-        # 1,2 or 3 from "G01", "G02" or "G03"
-        current_interpolation_mode = None
-
-        # 1 or 2 from "D01" or "D02"
-        # Note this is to support deprecated Gerber not putting
-        # an operation code at the end of every coordinate line.
-        current_operation_code = None
-
-        # Current coordinates
-        current_x = None
-        current_y = None
-        previous_x = None
-        previous_y = None
-
-        current_d = None
-
-        # Absolute or Relative/Incremental coordinates
-        # Not implemented
-        absolute = True
-
-        # How to interpret circular interpolation: SINGLE or MULTI
-        quadrant_mode = None
-
-        # Indicates we are parsing an aperture macro
-        current_macro = None
-
-        # Indicates the current polarity: D-Dark, C-Clear
-        current_polarity = 'D'
-
-        # If a region is being defined
-        making_region = False
-
-        # ### Parsing starts here ## ##
-        line_num = 0
-        gline = ""
-
-        s_tol = float(self.app.defaults["gerber_simp_tolerance"])
-
-        self.app.inform.emit('%s %d %s.' % (_("Gerber processing. Parsing"), len(glines), _("lines")))
-        try:
-            for gline in glines:
-                if self.app.abort_flag:
-                    # graceful abort requested by the user
-                    raise FlatCAMApp.GracefulException
-
-                line_num += 1
-                self.source_file += gline + '\n'
-
-                # Cleanup #
-                gline = gline.strip(' \r\n')
-                # log.debug("Line=%3s %s" % (line_num, gline))
-
-                # ###################
-                # Ignored lines #####
-                # Comments      #####
-                # ###################
-                match = self.comm_re.search(gline)
-                if match:
-                    continue
-
-                # Polarity change ###### ##
-                # Example: %LPD*% or %LPC*%
-                # If polarity changes, creates geometry from current
-                # buffer, then adds or subtracts accordingly.
-                match = self.lpol_re.search(gline)
-                if match:
-                    new_polarity = match.group(1)
-                    # log.info("Polarity CHANGE, LPC = %s, poly_buff = %s" % (self.is_lpc, poly_buffer))
-                    self.is_lpc = True if new_polarity == 'C' else False
-                    if len(path) > 1 and current_polarity != new_polarity:
-
-                        # finish the current path and add it to the storage
-                        # --- Buffered ----
-                        width = self.apertures[last_path_aperture]["size"]
-
-                        geo_dict = dict()
-                        geo_f = LineString(path)
-                        if not geo_f.is_empty:
-                            follow_buffer.append(geo_f)
-                            geo_dict['follow'] = geo_f
-
-                        geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                        if not geo_s.is_empty:
-                            if self.app.defaults['gerber_simplification']:
-                                poly_buffer.append(geo_s.simplify(s_tol))
-                            else:
-                                poly_buffer.append(geo_s)
-                            if self.is_lpc is True:
-                                geo_dict['clear'] = geo_s
-                            else:
-                                geo_dict['solid'] = geo_s
-
-                        if last_path_aperture not in self.apertures:
-                            self.apertures[last_path_aperture] = dict()
-                        if 'geometry' not in self.apertures[last_path_aperture]:
-                            self.apertures[last_path_aperture]['geometry'] = []
-                        self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
-
-                        path = [path[-1]]
-
-                    # --- Apply buffer ---
-                    # If added for testing of bug #83
-                    # TODO: Remove when bug fixed
-                    if len(poly_buffer) > 0:
-                        if current_polarity == 'D':
-                            # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer))
-                            self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
-
-                        else:
-                            # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer))
-                            self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
-
-                        # follow_buffer = []
-                        poly_buffer = []
-
-                    current_polarity = new_polarity
-                    continue
-
-                # ############################################################# ##
-                # Number format ############################################### ##
-                # Example: %FSLAX24Y24*%
-                # ############################################################# ##
-                # TODO: This is ignoring most of the format. Implement the rest.
-                match = self.fmt_re.search(gline)
-                if match:
-                    absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
-                    if match.group(1) is not None:
-                        self.gerber_zeros = match.group(1)
-                    self.int_digits = int(match.group(3))
-                    self.frac_digits = int(match.group(4))
-                    log.debug("Gerber format found. (%s) " % str(gline))
-
-                    log.debug(
-                        "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
-                        "D-no zero supression)" % self.gerber_zeros)
-                    log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
-                    continue
-
-                # ## Mode (IN/MM)
-                # Example: %MOIN*%
-                match = self.mode_re.search(gline)
-                if match:
-                    self.gerber_units = match.group(1)
-                    log.debug("Gerber units found = %s" % self.gerber_units)
-                    # Changed for issue #80
-                    self.convert_units(match.group(1))
-                    continue
-
-                # ############################################################# ##
-                # Combined Number format and Mode --- Allegro does this ####### ##
-                # ############################################################# ##
-                match = self.fmt_re_alt.search(gline)
-                if match:
-                    absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
-                    if match.group(1) is not None:
-                        self.gerber_zeros = match.group(1)
-                    self.int_digits = int(match.group(3))
-                    self.frac_digits = int(match.group(4))
-                    log.debug("Gerber format found. (%s) " % str(gline))
-                    log.debug(
-                        "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
-                        "D-no zero suppression)" % self.gerber_zeros)
-                    log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
-
-                    self.gerber_units = match.group(5)
-                    log.debug("Gerber units found = %s" % self.gerber_units)
-                    # Changed for issue #80
-                    self.convert_units(match.group(5))
-                    continue
-
-                # ############################################################# ##
-                # Search for OrCAD way for having Number format
-                # ############################################################# ##
-                match = self.fmt_re_orcad.search(gline)
-                if match:
-                    if match.group(1) is not None:
-                        if match.group(1) == 'G74':
-                            quadrant_mode = 'SINGLE'
-                        elif match.group(1) == 'G75':
-                            quadrant_mode = 'MULTI'
-                        absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(3)]
-                        if match.group(2) is not None:
-                            self.gerber_zeros = match.group(2)
-
-                        self.int_digits = int(match.group(4))
-                        self.frac_digits = int(match.group(5))
-                        log.debug("Gerber format found. (%s) " % str(gline))
-                        log.debug(
-                            "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
-                            "D-no zerosuppressionn)" % self.gerber_zeros)
-                        log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
-
-                        self.gerber_units = match.group(1)
-                        log.debug("Gerber units found = %s" % self.gerber_units)
-                        # Changed for issue #80
-                        self.convert_units(match.group(5))
-                        continue
-
-                # ############################################################# ##
-                # Units (G70/1) OBSOLETE
-                # ############################################################# ##
-                match = self.units_re.search(gline)
-                if match:
-                    obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)]
-                    log.warning("Gerber obsolete units found = %s" % obs_gerber_units)
-                    # Changed for issue #80
-                    self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
-                    continue
-
-                # ############################################################# ##
-                # Absolute/relative coordinates G90/1 OBSOLETE ######## ##
-                # ##################################################### ##
-                match = self.absrel_re.search(gline)
-                if match:
-                    absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
-                    log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
-                    continue
-
-                # ############################################################# ##
-                # Aperture Macros ##################################### ##
-                # Having this at the beginning will slow things down
-                # but macros can have complicated statements than could
-                # be caught by other patterns.
-                # ############################################################# ##
-                if current_macro is None:  # No macro started yet
-                    match = self.am1_re.search(gline)
-                    # Start macro if match, else not an AM, carry on.
-                    if match:
-                        log.debug("Starting macro. Line %d: %s" % (line_num, gline))
-                        current_macro = match.group(1)
-                        self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
-                        if match.group(2):  # Append
-                            self.aperture_macros[current_macro].append(match.group(2))
-                        if match.group(3):  # Finish macro
-                            # self.aperture_macros[current_macro].parse_content()
-                            current_macro = None
-                            log.debug("Macro complete in 1 line.")
-                        continue
-                else:  # Continue macro
-                    log.debug("Continuing macro. Line %d." % line_num)
-                    match = self.am2_re.search(gline)
-                    if match:  # Finish macro
-                        log.debug("End of macro. Line %d." % line_num)
-                        self.aperture_macros[current_macro].append(match.group(1))
-                        # self.aperture_macros[current_macro].parse_content()
-                        current_macro = None
-                    else:  # Append
-                        self.aperture_macros[current_macro].append(gline)
-                    continue
-
-                # ## Aperture definitions %ADD...
-                match = self.ad_re.search(gline)
-                if match:
-                    # log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
-                    self.aperture_parse(match.group(1), match.group(2), match.group(3))
-                    continue
-
-                # ############################################################# ##
-                # Operation code alone ###################### ##
-                # Operation code alone, usually just D03 (Flash)
-                # self.opcode_re = re.compile(r'^D0?([123])\*$')
-                # ############################################################# ##
-                match = self.opcode_re.search(gline)
-                if match:
-                    current_operation_code = int(match.group(1))
-                    current_d = current_operation_code
-
-                    if current_operation_code == 3:
-
-                        # --- Buffered ---
-                        try:
-                            log.debug("Bare op-code %d." % current_operation_code)
-                            geo_dict = dict()
-                            flash = self.create_flash_geometry(
-                                Point(current_x, current_y), self.apertures[current_aperture],
-                                self.steps_per_circle)
-
-                            geo_dict['follow'] = Point([current_x, current_y])
-
-                            if not flash.is_empty:
-                                if self.app.defaults['gerber_simplification']:
-                                    poly_buffer.append(flash.simplify(s_tol))
-                                else:
-                                    poly_buffer.append(flash)
-                                if self.is_lpc is True:
-                                    geo_dict['clear'] = flash
-                                else:
-                                    geo_dict['solid'] = flash
-
-                                if current_aperture not in self.apertures:
-                                    self.apertures[current_aperture] = dict()
-                                if 'geometry' not in self.apertures[current_aperture]:
-                                    self.apertures[current_aperture]['geometry'] = []
-                                self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
-
-                        except IndexError:
-                            log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
-
-                    continue
-
-                # ############################################################# ##
-                # Tool/aperture change
-                # Example: D12*
-                # ############################################################# ##
-                match = self.tool_re.search(gline)
-                if match:
-                    current_aperture = match.group(1)
-                    # log.debug("Line %d: Aperture change to (%s)" % (line_num, current_aperture))
-
-                    # If the aperture value is zero then make it something quite small but with a non-zero value
-                    # so it can be processed by FlatCAM.
-                    # But first test to see if the aperture type is "aperture macro". In that case
-                    # we should not test for "size" key as it does not exist in this case.
-                    if self.apertures[current_aperture]["type"] is not "AM":
-                        if self.apertures[current_aperture]["size"] == 0:
-                            self.apertures[current_aperture]["size"] = 1e-12
-                    # log.debug(self.apertures[current_aperture])
-
-                    # Take care of the current path with the previous tool
-                    if len(path) > 1:
-                        if self.apertures[last_path_aperture]["type"] == 'R':
-                            # do nothing because 'R' type moving aperture is none at once
-                            pass
-                        else:
-                            geo_dict = dict()
-                            geo_f = LineString(path)
-                            if not geo_f.is_empty:
-                                follow_buffer.append(geo_f)
-                                geo_dict['follow'] = geo_f
-
-                            # --- Buffered ----
-                            width = self.apertures[last_path_aperture]["size"]
-                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                            if not geo_s.is_empty:
-                                if self.app.defaults['gerber_simplification']:
-                                    poly_buffer.append(geo_s.simplify(s_tol))
-                                else:
-                                    poly_buffer.append(geo_s)
-                                if self.is_lpc is True:
-                                    geo_dict['clear'] = geo_s
-                                else:
-                                    geo_dict['solid'] = geo_s
-
-                            if last_path_aperture not in self.apertures:
-                                self.apertures[last_path_aperture] = dict()
-                            if 'geometry' not in self.apertures[last_path_aperture]:
-                                self.apertures[last_path_aperture]['geometry'] = []
-                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
-
-                            path = [path[-1]]
-
-                    continue
-
-                # ############################################################# ##
-                # G36* - Begin region
-                # ############################################################# ##
-                if self.regionon_re.search(gline):
-                    if len(path) > 1:
-                        # Take care of what is left in the path
-
-                        geo_dict = dict()
-                        geo_f = LineString(path)
-                        if not geo_f.is_empty:
-                            follow_buffer.append(geo_f)
-                            geo_dict['follow'] = geo_f
-
-                        # --- Buffered ----
-                        width = self.apertures[last_path_aperture]["size"]
-                        geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                        if not geo_s.is_empty:
-                            if self.app.defaults['gerber_simplification']:
-                                poly_buffer.append(geo_s.simplify(s_tol))
-                            else:
-                                poly_buffer.append(geo_s)
-                            if self.is_lpc is True:
-                                geo_dict['clear'] = geo_s
-                            else:
-                                geo_dict['solid'] = geo_s
-
-                        if last_path_aperture not in self.apertures:
-                            self.apertures[last_path_aperture] = dict()
-                        if 'geometry' not in self.apertures[last_path_aperture]:
-                            self.apertures[last_path_aperture]['geometry'] = []
-                        self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
-
-                        path = [path[-1]]
-
-                    making_region = True
-                    continue
-
-                # ############################################################# ##
-                # G37* - End region
-                # ############################################################# ##
-                if self.regionoff_re.search(gline):
-                    making_region = False
-
-                    if '0' not in self.apertures:
-                        self.apertures['0'] = {}
-                        self.apertures['0']['type'] = 'REG'
-                        self.apertures['0']['size'] = 0.0
-                        self.apertures['0']['geometry'] = []
-
-                    # if D02 happened before G37 we now have a path with 1 element only; we have to add the current
-                    # geo to the poly_buffer otherwise we loose it
-                    if current_operation_code == 2:
-                        if len(path) == 1:
-                            # this means that the geometry was prepared previously and we just need to add it
-                            geo_dict = dict()
-                            if geo_f:
-                                if not geo_f.is_empty:
-                                    follow_buffer.append(geo_f)
-                                    geo_dict['follow'] = geo_f
-                            if geo_s:
-                                if not geo_s.is_empty:
-                                    if self.app.defaults['gerber_simplification']:
-                                        poly_buffer.append(geo_s.simplify(s_tol))
-                                    else:
-                                        poly_buffer.append(geo_s)
-                                    if self.is_lpc is True:
-                                        geo_dict['clear'] = geo_s
-                                    else:
-                                        geo_dict['solid'] = geo_s
-
-                            if geo_s or geo_f:
-                                self.apertures['0']['geometry'].append(deepcopy(geo_dict))
-
-                            path = [[current_x, current_y]]  # Start new path
-
-                    # Only one path defines region?
-                    # This can happen if D02 happened before G37 and
-                    # is not and error.
-                    if len(path) < 3:
-                        # print "ERROR: Path contains less than 3 points:"
-                        # path = [[current_x, current_y]]
-                        continue
-
-                    # For regions we may ignore an aperture that is None
-
-                    # --- Buffered ---
-                    geo_dict = dict()
-                    region_f = Polygon(path).exterior
-                    if not region_f.is_empty:
-                        follow_buffer.append(region_f)
-                        geo_dict['follow'] = region_f
-
-                    region_s = Polygon(path)
-                    if not region_s.is_valid:
-                        region_s = region_s.buffer(0, int(self.steps_per_circle / 4))
-
-                    if not region_s.is_empty:
-                        if self.app.defaults['gerber_simplification']:
-                            poly_buffer.append(region_s.simplify(s_tol))
-                        else:
-                            poly_buffer.append(region_s)
-                        if self.is_lpc is True:
-                            geo_dict['clear'] = region_s
-                        else:
-                            geo_dict['solid'] = region_s
-
-                    if not region_s.is_empty or not region_f.is_empty:
-                        self.apertures['0']['geometry'].append(deepcopy(geo_dict))
-
-                    path = [[current_x, current_y]]  # Start new path
-                    continue
-
-                # ## G01/2/3* - Interpolation mode change
-                # Can occur along with coordinates and operation code but
-                # sometimes by itself (handled here).
-                # Example: G01*
-                match = self.interp_re.search(gline)
-                if match:
-                    current_interpolation_mode = int(match.group(1))
-                    continue
-
-                # ## G01 - Linear interpolation plus flashes
-                # Operation code (D0x) missing is deprecated... oh well I will support it.
-                # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
-                match = self.lin_re.search(gline)
-                if match:
-                    # Dxx alone?
-                    # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
-                    #     try:
-                    #         current_operation_code = int(match.group(4))
-                    #     except:
-                    #         pass  # A line with just * will match too.
-                    #     continue
-                    # NOTE: Letting it continue allows it to react to the
-                    #       operation code.
-
-                    # Parse coordinates
-                    if match.group(2) is not None:
-                        linear_x = parse_gerber_number(match.group(2),
-                                                       self.int_digits, self.frac_digits, self.gerber_zeros)
-                        current_x = linear_x
-                    else:
-                        linear_x = current_x
-                    if match.group(3) is not None:
-                        linear_y = parse_gerber_number(match.group(3),
-                                                       self.int_digits, self.frac_digits, self.gerber_zeros)
-                        current_y = linear_y
-                    else:
-                        linear_y = current_y
-
-                    # Parse operation code
-                    if match.group(4) is not None:
-                        current_operation_code = int(match.group(4))
-
-                    # Pen down: add segment
-                    if current_operation_code == 1:
-                        # if linear_x or linear_y are None, ignore those
-                        if current_x is not None and current_y is not None:
-                            # only add the point if it's a new one otherwise skip it (harder to process)
-                            if path[-1] != [current_x, current_y]:
-                                path.append([current_x, current_y])
-
-                            if making_region is False:
-                                # if the aperture is rectangle then add a rectangular shape having as parameters the
-                                # coordinates of the start and end point and also the width and height
-                                # of the 'R' aperture
-                                try:
-                                    if self.apertures[current_aperture]["type"] == 'R':
-                                        width = self.apertures[current_aperture]['width']
-                                        height = self.apertures[current_aperture]['height']
-                                        minx = min(path[0][0], path[1][0]) - width / 2
-                                        maxx = max(path[0][0], path[1][0]) + width / 2
-                                        miny = min(path[0][1], path[1][1]) - height / 2
-                                        maxy = max(path[0][1], path[1][1]) + height / 2
-                                        log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
-
-                                        geo_dict = dict()
-                                        geo_f = Point([current_x, current_y])
-                                        follow_buffer.append(geo_f)
-                                        geo_dict['follow'] = geo_f
-
-                                        geo_s = shply_box(minx, miny, maxx, maxy)
-                                        if self.app.defaults['gerber_simplification']:
-                                            poly_buffer.append(geo_s.simplify(s_tol))
-                                        else:
-                                            poly_buffer.append(geo_s)
-
-                                        if self.is_lpc is True:
-                                            geo_dict['clear'] = geo_s
-                                        else:
-                                            geo_dict['solid'] = geo_s
-
-                                        if current_aperture not in self.apertures:
-                                            self.apertures[current_aperture] = dict()
-                                        if 'geometry' not in self.apertures[current_aperture]:
-                                            self.apertures[current_aperture]['geometry'] = []
-                                        self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
-                                except Exception as e:
-                                    pass
-                            last_path_aperture = current_aperture
-                            # we do this for the case that a region is done without having defined any aperture
-                            if last_path_aperture is None:
-                                if '0' not in self.apertures:
-                                    self.apertures['0'] = {}
-                                    self.apertures['0']['type'] = 'REG'
-                                    self.apertures['0']['size'] = 0.0
-                                    self.apertures['0']['geometry'] = []
-                                last_path_aperture = '0'
-                        else:
-                            self.app.inform.emit('[WARNING] %s: %s' %
-                                                 (_("Coordinates missing, line ignored"), str(gline)))
-                            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                                 _("GERBER file might be CORRUPT. Check the file !!!"))
-
-                    elif current_operation_code == 2:
-                        if len(path) > 1:
-                            geo_s = None
-                            geo_f = None
-
-                            geo_dict = dict()
-                            # --- BUFFERED ---
-                            # this treats the case when we are storing geometry as paths only
-                            if making_region:
-                                # we do this for the case that a region is done without having defined any aperture
-                                if last_path_aperture is None:
-                                    if '0' not in self.apertures:
-                                        self.apertures['0'] = {}
-                                        self.apertures['0']['type'] = 'REG'
-                                        self.apertures['0']['size'] = 0.0
-                                        self.apertures['0']['geometry'] = []
-                                    last_path_aperture = '0'
-                                geo_f = Polygon()
-                            else:
-                                geo_f = LineString(path)
-
-                            try:
-                                if self.apertures[last_path_aperture]["type"] != 'R':
-                                    if not geo_f.is_empty:
-                                        follow_buffer.append(geo_f)
-                                        geo_dict['follow'] = geo_f
-                            except Exception as e:
-                                log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
-                                if not geo_f.is_empty:
-                                    follow_buffer.append(geo_f)
-                                    geo_dict['follow'] = geo_f
-
-                            # this treats the case when we are storing geometry as solids
-                            if making_region:
-                                # we do this for the case that a region is done without having defined any aperture
-                                if last_path_aperture is None:
-                                    if '0' not in self.apertures:
-                                        self.apertures['0'] = {}
-                                        self.apertures['0']['type'] = 'REG'
-                                        self.apertures['0']['size'] = 0.0
-                                        self.apertures['0']['geometry'] = []
-                                    last_path_aperture = '0'
-
-                                try:
-                                    geo_s = Polygon(path)
-                                except ValueError:
-                                    log.warning("Problem %s %s" % (gline, line_num))
-                                    self.app.inform.emit('[ERROR] %s: %s' %
-                                                         (_("Region does not have enough points. "
-                                                            "File will be processed but there are parser errors. "
-                                                            "Line number"), str(line_num)))
-                            else:
-                                if last_path_aperture is None:
-                                    log.warning("No aperture defined for curent path. (%d)" % line_num)
-                                width = self.apertures[last_path_aperture]["size"]  # TODO: WARNING this should fail!
-                                geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-
-                            try:
-                                if self.apertures[last_path_aperture]["type"] != 'R':
-                                    if not geo_s.is_empty:
-                                        if self.app.defaults['gerber_simplification']:
-                                            poly_buffer.append(geo_s.simplify(s_tol))
-                                        else:
-                                            poly_buffer.append(geo_s)
-
-                                        if self.is_lpc is True:
-                                            geo_dict['clear'] = geo_s
-                                        else:
-                                            geo_dict['solid'] = geo_s
-                            except Exception as e:
-                                log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
-                                if self.app.defaults['gerber_simplification']:
-                                    poly_buffer.append(geo_s.simplify(s_tol))
-                                else:
-                                    poly_buffer.append(geo_s)
-
-                                if self.is_lpc is True:
-                                    geo_dict['clear'] = geo_s
-                                else:
-                                    geo_dict['solid'] = geo_s
-
-                            if last_path_aperture not in self.apertures:
-                                self.apertures[last_path_aperture] = dict()
-                            if 'geometry' not in self.apertures[last_path_aperture]:
-                                self.apertures[last_path_aperture]['geometry'] = []
-                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
-
-                        # if linear_x or linear_y are None, ignore those
-                        if linear_x is not None and linear_y is not None:
-                            path = [[linear_x, linear_y]]  # Start new path
-                        else:
-                            self.app.inform.emit('[WARNING] %s: %s' %
-                                                 (_("Coordinates missing, line ignored"), str(gline)))
-                            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                                 _("GERBER file might be CORRUPT. Check the file !!!"))
-
-                    # Flash
-                    # Not allowed in region mode.
-                    elif current_operation_code == 3:
-
-                        # Create path draw so far.
-                        if len(path) > 1:
-                            # --- Buffered ----
-                            geo_dict = dict()
-
-                            # this treats the case when we are storing geometry as paths
-                            geo_f = LineString(path)
-                            if not geo_f.is_empty:
-                                try:
-                                    if self.apertures[last_path_aperture]["type"] != 'R':
-                                        follow_buffer.append(geo_f)
-                                        geo_dict['follow'] = geo_f
-                                except Exception as e:
-                                    log.debug("camlib.Gerber.parse_lines() --> G01 match D03 --> %s" % str(e))
-                                    follow_buffer.append(geo_f)
-                                    geo_dict['follow'] = geo_f
-
-                            # this treats the case when we are storing geometry as solids
-                            width = self.apertures[last_path_aperture]["size"]
-                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                            if not geo_s.is_empty:
-                                try:
-                                    if self.apertures[last_path_aperture]["type"] != 'R':
-                                        if self.app.defaults['gerber_simplification']:
-                                            poly_buffer.append(geo_s.simplify(s_tol))
-                                        else:
-                                            poly_buffer.append(geo_s)
-
-                                        if self.is_lpc is True:
-                                            geo_dict['clear'] = geo_s
-                                        else:
-                                            geo_dict['solid'] = geo_s
-                                except:
-                                    if self.app.defaults['gerber_simplification']:
-                                        poly_buffer.append(geo_s.simplify(s_tol))
-                                    else:
-                                        poly_buffer.append(geo_s)
-
-                                    if self.is_lpc is True:
-                                        geo_dict['clear'] = geo_s
-                                    else:
-                                        geo_dict['solid'] = geo_s
-
-                            if last_path_aperture not in self.apertures:
-                                self.apertures[last_path_aperture] = dict()
-                            if 'geometry' not in self.apertures[last_path_aperture]:
-                                self.apertures[last_path_aperture]['geometry'] = []
-                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
-
-                        # Reset path starting point
-                        path = [[linear_x, linear_y]]
-
-                        # --- BUFFERED ---
-                        # Draw the flash
-                        # this treats the case when we are storing geometry as paths
-                        geo_dict = dict()
-                        geo_flash = Point([linear_x, linear_y])
-                        follow_buffer.append(geo_flash)
-                        geo_dict['follow'] = geo_flash
-
-                        # this treats the case when we are storing geometry as solids
-                        flash = self.create_flash_geometry(
-                            Point([linear_x, linear_y]),
-                            self.apertures[current_aperture],
-                            self.steps_per_circle
-                        )
-                        if not flash.is_empty:
-                            if self.app.defaults['gerber_simplification']:
-                                poly_buffer.append(flash.simplify(s_tol))
-                            else:
-                                poly_buffer.append(flash)
-
-                            if self.is_lpc is True:
-                                geo_dict['clear'] = flash
-                            else:
-                                geo_dict['solid'] = flash
-
-                        if current_aperture not in self.apertures:
-                            self.apertures[current_aperture] = dict()
-                        if 'geometry' not in self.apertures[current_aperture]:
-                            self.apertures[current_aperture]['geometry'] = []
-                        self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
-
-                    # maybe those lines are not exactly needed but it is easier to read the program as those coordinates
-                    # are used in case that circular interpolation is encountered within the Gerber file
-                    current_x = linear_x
-                    current_y = linear_y
-
-                    # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
-                    continue
-
-                # ## G74/75* - Single or multiple quadrant arcs
-                match = self.quad_re.search(gline)
-                if match:
-                    if match.group(1) == '4':
-                        quadrant_mode = 'SINGLE'
-                    else:
-                        quadrant_mode = 'MULTI'
-                    continue
-
-                # ## G02/3 - Circular interpolation
-                # 2-clockwise, 3-counterclockwise
-                # Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point
-                match = self.circ_re.search(gline)
-                if match:
-                    arcdir = [None, None, "cw", "ccw"]
-
-                    mode, circular_x, circular_y, i, j, d = match.groups()
-
-                    try:
-                        circular_x = parse_gerber_number(circular_x,
-                                                         self.int_digits, self.frac_digits, self.gerber_zeros)
-                    except:
-                        circular_x = current_x
-
-                    try:
-                        circular_y = parse_gerber_number(circular_y,
-                                                         self.int_digits, self.frac_digits, self.gerber_zeros)
-                    except:
-                        circular_y = current_y
-
-                    # According to Gerber specification i and j are not modal, which means that when i or j are missing,
-                    # they are to be interpreted as being zero
-                    try:
-                        i = parse_gerber_number(i, self.int_digits, self.frac_digits, self.gerber_zeros)
-                    except:
-                        i = 0
-
-                    try:
-                        j = parse_gerber_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
-                    except:
-                        j = 0
-
-                    if quadrant_mode is None:
-                        log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
-                        log.error(gline)
-                        continue
-
-                    if mode is None and current_interpolation_mode not in [2, 3]:
-                        log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
-                        log.error(gline)
-                        continue
-                    elif mode is not None:
-                        current_interpolation_mode = int(mode)
-
-                    # Set operation code if provided
-                    if d is not None:
-                        current_operation_code = int(d)
-
-                    # Nothing created! Pen Up.
-                    if current_operation_code == 2:
-                        log.warning("Arc with D2. (%d)" % line_num)
-                        if len(path) > 1:
-                            geo_dict = dict()
-
-                            if last_path_aperture is None:
-                                log.warning("No aperture defined for curent path. (%d)" % line_num)
-
-                            # --- BUFFERED ---
-                            width = self.apertures[last_path_aperture]["size"]
-
-                            # this treats the case when we are storing geometry as paths
-                            geo_f = LineString(path)
-                            if not geo_f.is_empty:
-                                follow_buffer.append(geo_f)
-                                geo_dict['follow'] = geo_f
-
-                            # this treats the case when we are storing geometry as solids
-                            buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
-                            if not buffered.is_empty:
-                                if self.app.defaults['gerber_simplification']:
-                                    poly_buffer.append(buffered.simplify(s_tol))
-                                else:
-                                    poly_buffer.append(buffered)
-
-                                if self.is_lpc is True:
-                                    geo_dict['clear'] = buffered
-                                else:
-                                    geo_dict['solid'] = buffered
-
-                            if last_path_aperture not in self.apertures:
-                                self.apertures[last_path_aperture] = dict()
-                            if 'geometry' not in self.apertures[last_path_aperture]:
-                                self.apertures[last_path_aperture]['geometry'] = []
-                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
-
-                        current_x = circular_x
-                        current_y = circular_y
-                        path = [[current_x, current_y]]  # Start new path
-                        continue
-
-                    # Flash should not happen here
-                    if current_operation_code == 3:
-                        log.error("Trying to flash within arc. (%d)" % line_num)
-                        continue
-
-                    if quadrant_mode == 'MULTI':
-                        center = [i + current_x, j + current_y]
-                        radius = sqrt(i ** 2 + j ** 2)
-                        start = arctan2(-j, -i)  # Start angle
-                        # Numerical errors might prevent start == stop therefore
-                        # we check ahead of time. This should result in a
-                        # 360 degree arc.
-                        if current_x == circular_x and current_y == circular_y:
-                            stop = start
-                        else:
-                            stop = arctan2(-center[1] + circular_y, -center[0] + circular_x)  # Stop angle
-
-                        this_arc = arc(center, radius, start, stop,
-                                       arcdir[current_interpolation_mode],
-                                       self.steps_per_circle)
-
-                        # The last point in the computed arc can have
-                        # numerical errors. The exact final point is the
-                        # specified (x, y). Replace.
-                        this_arc[-1] = (circular_x, circular_y)
-
-                        # Last point in path is current point
-                        # current_x = this_arc[-1][0]
-                        # current_y = this_arc[-1][1]
-                        current_x, current_y = circular_x, circular_y
-
-                        # Append
-                        path += this_arc
-                        last_path_aperture = current_aperture
-
-                        continue
-
-                    if quadrant_mode == 'SINGLE':
-
-                        center_candidates = [
-                            [i + current_x, j + current_y],
-                            [-i + current_x, j + current_y],
-                            [i + current_x, -j + current_y],
-                            [-i + current_x, -j + current_y]
-                        ]
-
-                        valid = False
-                        log.debug("I: %f  J: %f" % (i, j))
-                        for center in center_candidates:
-                            radius = sqrt(i ** 2 + j ** 2)
-
-                            # Make sure radius to start is the same as radius to end.
-                            radius2 = sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
-                            if radius2 < radius * 0.95 or radius2 > radius * 1.05:
-                                continue  # Not a valid center.
-
-                            # Correct i and j and continue as with multi-quadrant.
-                            i = center[0] - current_x
-                            j = center[1] - current_y
-
-                            start = arctan2(-j, -i)  # Start angle
-                            stop = arctan2(-center[1] + circular_y, -center[0] + circular_x)  # Stop angle
-                            angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
-                            log.debug("ARC START: %f, %f  CENTER: %f, %f  STOP: %f, %f" %
-                                      (current_x, current_y, center[0], center[1], circular_x, circular_y))
-                            log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
-                                      (start * 180 / pi, stop * 180 / pi, arcdir[current_interpolation_mode],
-                                       angle * 180 / pi, pi / 2 * 180 / pi, angle <= (pi + 1e-6) / 2))
-
-                            if angle <= (pi + 1e-6) / 2:
-                                log.debug("########## ACCEPTING ARC ############")
-                                this_arc = arc(center, radius, start, stop,
-                                               arcdir[current_interpolation_mode],
-                                               self.steps_per_circle)
-
-                                # Replace with exact values
-                                this_arc[-1] = (circular_x, circular_y)
-
-                                # current_x = this_arc[-1][0]
-                                # current_y = this_arc[-1][1]
-                                current_x, current_y = circular_x, circular_y
-
-                                path += this_arc
-                                last_path_aperture = current_aperture
-                                valid = True
-                                break
-
-                        if valid:
-                            continue
-                        else:
-                            log.warning("Invalid arc in line %d." % line_num)
-
-                # ## EOF
-                match = self.eof_re.search(gline)
-                if match:
-                    continue
-
-                # ## Line did not match any pattern. Warn user.
-                log.warning("Line ignored (%d): %s" % (line_num, gline))
-
-            if len(path) > 1:
-                # In case that G01 (moving) aperture is rectangular, there is no need to still create
-                # another geo since we already created a shapely box using the start and end coordinates found in
-                # path variable. We do it only for other apertures than 'R' type
-                if self.apertures[last_path_aperture]["type"] == 'R':
-                    pass
-                else:
-                    # EOF, create shapely LineString if something still in path
-                    # ## --- Buffered ---
-
-                    geo_dict = dict()
-                    # this treats the case when we are storing geometry as paths
-                    geo_f = LineString(path)
-                    if not geo_f.is_empty:
-                        follow_buffer.append(geo_f)
-                        geo_dict['follow'] = geo_f
-
-                    # this treats the case when we are storing geometry as solids
-                    width = self.apertures[last_path_aperture]["size"]
-                    geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
-                    if not geo_s.is_empty:
-                        if self.app.defaults['gerber_simplification']:
-                            poly_buffer.append(geo_s.simplify(s_tol))
-                        else:
-                            poly_buffer.append(geo_s)
-
-                        if self.is_lpc is True:
-                            geo_dict['clear'] = geo_s
-                        else:
-                            geo_dict['solid'] = geo_s
-
-                    if last_path_aperture not in self.apertures:
-                        self.apertures[last_path_aperture] = dict()
-                    if 'geometry' not in self.apertures[last_path_aperture]:
-                        self.apertures[last_path_aperture]['geometry'] = []
-                    self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
-
-            # TODO: make sure to keep track of units changes because right now it seems to happen in a weird way
-            # find out the conversion factor used to convert inside the self.apertures keys: size, width, height
-            file_units = self.gerber_units if self.gerber_units else 'IN'
-            app_units = self.app.defaults['units']
-
-            conversion_factor = 25.4 if file_units == 'IN' else (1/25.4) if file_units != app_units else 1
-
-            # --- Apply buffer ---
-            # this treats the case when we are storing geometry as paths
-            self.follow_geometry = follow_buffer
-
-            # this treats the case when we are storing geometry as solids
-
-            if len(poly_buffer) == 0:
-                log.error("Object is not Gerber file or empty. Aborting Object creation.")
-                return 'fail'
-
-            log.warning("Joining %d polygons." % len(poly_buffer))
-            self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(poly_buffer)))
-
-            if self.use_buffer_for_union:
-                log.debug("Union by buffer...")
-
-                new_poly = MultiPolygon(poly_buffer)
-                if self.app.defaults["gerber_buffering"] == 'full':
-                    new_poly = new_poly.buffer(0.00000001)
-                    new_poly = new_poly.buffer(-0.00000001)
-                log.warning("Union(buffer) done.")
-            else:
-                log.debug("Union by union()...")
-                new_poly = cascaded_union(poly_buffer)
-                new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
-                log.warning("Union done.")
-
-            if current_polarity == 'D':
-                self.app.inform.emit('%s' % _("Gerber processing. Applying Gerber polarity."))
-                if new_poly.is_valid:
-                    self.solid_geometry = self.solid_geometry.union(new_poly)
-                else:
-                    # I do this so whenever the parsed geometry of the file is not valid (intersections) it is still
-                    # loaded. Instead of applying a union I add to a list of polygons.
-                    final_poly = []
-                    try:
-                        for poly in new_poly:
-                            final_poly.append(poly)
-                    except TypeError:
-                        final_poly.append(new_poly)
-
-                    try:
-                        for poly in self.solid_geometry:
-                            final_poly.append(poly)
-                    except TypeError:
-                        final_poly.append(self.solid_geometry)
-
-                    self.solid_geometry = final_poly
-
-                # try:
-                #     self.solid_geometry = self.solid_geometry.union(new_poly)
-                # except Exception as e:
-                #     # in case in the new_poly are some self intersections try to avoid making union with them
-                #     for poly in new_poly:
-                #         try:
-                #             self.solid_geometry = self.solid_geometry.union(poly)
-                #         except:
-                #             pass
-            else:
-                self.solid_geometry = self.solid_geometry.difference(new_poly)
-        except Exception as err:
-            ex_type, ex, tb = sys.exc_info()
-            traceback.print_tb(tb)
-            # print traceback.format_exc()
-
-            log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline))
-
-            loc = '%s #%d %s: %s\n' % (_("Gerber Line"), line_num, _("Gerber Line Content"), gline) + repr(err)
-            self.app.inform.emit('[ERROR] %s\n%s:' %
-                                 (_("Gerber Parser ERROR"), loc))
-
-    @staticmethod
-    def create_flash_geometry(location, aperture, steps_per_circle=None):
-
-        # log.debug('Flashing @%s, Aperture: %s' % (location, aperture))
-
-        if type(location) == list:
-            location = Point(location)
-
-        if aperture['type'] == 'C':  # Circles
-            return location.buffer(aperture['size'] / 2, int(steps_per_circle / 4))
-
-        if aperture['type'] == 'R':  # Rectangles
-            loc = location.coords[0]
-            width = aperture['width']
-            height = aperture['height']
-            minx = loc[0] - width / 2
-            maxx = loc[0] + width / 2
-            miny = loc[1] - height / 2
-            maxy = loc[1] + height / 2
-            return shply_box(minx, miny, maxx, maxy)
-
-        if aperture['type'] == 'O':  # Obround
-            loc = location.coords[0]
-            width = aperture['width']
-            height = aperture['height']
-            if width > height:
-                p1 = Point(loc[0] + 0.5 * (width - height), loc[1])
-                p2 = Point(loc[0] - 0.5 * (width - height), loc[1])
-                c1 = p1.buffer(height * 0.5, int(steps_per_circle / 4))
-                c2 = p2.buffer(height * 0.5, int(steps_per_circle / 4))
-            else:
-                p1 = Point(loc[0], loc[1] + 0.5 * (height - width))
-                p2 = Point(loc[0], loc[1] - 0.5 * (height - width))
-                c1 = p1.buffer(width * 0.5, int(steps_per_circle / 4))
-                c2 = p2.buffer(width * 0.5, int(steps_per_circle / 4))
-            return cascaded_union([c1, c2]).convex_hull
-
-        if aperture['type'] == 'P':  # Regular polygon
-            loc = location.coords[0]
-            diam = aperture['diam']
-            n_vertices = aperture['nVertices']
-            points = []
-            for i in range(0, n_vertices):
-                x = loc[0] + 0.5 * diam * (cos(2 * pi * i / n_vertices))
-                y = loc[1] + 0.5 * diam * (sin(2 * pi * i / n_vertices))
-                points.append((x, y))
-            ply = Polygon(points)
-            if 'rotation' in aperture:
-                ply = affinity.rotate(ply, aperture['rotation'])
-            return ply
-
-        if aperture['type'] == 'AM':  # Aperture Macro
-            loc = location.coords[0]
-            flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
-            if flash_geo.is_empty:
-                log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name))
-            return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
-
-        log.warning("Unknown aperture type: %s" % aperture['type'])
-        return None
-    
-    def create_geometry(self):
-        """
-        Geometry from a Gerber file is made up entirely of polygons.
-        Every stroke (linear or circular) has an aperture which gives
-        it thickness. Additionally, aperture strokes have non-zero area,
-        and regions naturally do as well.
-
-        :rtype : None
-        :return: None
-        """
-        pass
-        # self.buffer_paths()
-        #
-        # self.fix_regions()
-        #
-        # self.do_flashes()
-        #
-        # self.solid_geometry = cascaded_union(self.buffered_paths +
-        #                                      [poly['polygon'] for poly in self.regions] +
-        #                                      self.flash_geometry)
-
-    def get_bounding_box(self, margin=0.0, rounded=False):
-        """
-        Creates and returns a rectangular polygon bounding at a distance of
-        margin from the object's ``solid_geometry``. If margin > 0, the polygon
-        can optionally have rounded corners of radius equal to margin.
-
-        :param margin: Distance to enlarge the rectangular bounding
-         box in both positive and negative, x and y axes.
-        :type margin: float
-        :param rounded: Wether or not to have rounded corners.
-        :type rounded: bool
-        :return: The bounding box.
-        :rtype: Shapely.Polygon
-        """
-
-        bbox = self.solid_geometry.envelope.buffer(margin)
-        if not rounded:
-            bbox = bbox.envelope
-        return bbox
-
-    def bounds(self):
-        """
-        Returns coordinates of rectangular bounds
-        of Gerber geometry: (xmin, ymin, xmax, ymax).
-        """
-        # fixed issue of getting bounds only for one level lists of objects
-        # now it can get bounds for nested lists of objects
-
-        log.debug("camlib.Gerber.bounds()")
-
-        if self.solid_geometry is None:
-            log.debug("solid_geometry is None")
-            return 0, 0, 0, 0
-
-        def bounds_rec(obj):
-            if type(obj) is list and type(obj) is not MultiPolygon:
-                minx = Inf
-                miny = Inf
-                maxx = -Inf
-                maxy = -Inf
-
-                for k in obj:
-                    if type(k) is dict:
-                        for key in k:
-                            minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
-                            minx = min(minx, minx_)
-                            miny = min(miny, miny_)
-                            maxx = max(maxx, maxx_)
-                            maxy = max(maxy, maxy_)
-                    else:
-                        if not k.is_empty:
-                            try:
-                                minx_, miny_, maxx_, maxy_ = bounds_rec(k)
-                            except Exception as e:
-                                log.debug("camlib.Gerber.bounds() --> %s" % str(e))
-                                return
-
-                            minx = min(minx, minx_)
-                            miny = min(miny, miny_)
-                            maxx = max(maxx, maxx_)
-                            maxy = max(maxy, maxy_)
-                return minx, miny, maxx, maxy
-            else:
-                # it's a Shapely object, return it's bounds
-                return obj.bounds
-
-        bounds_coords = bounds_rec(self.solid_geometry)
-        return bounds_coords
-
-    def scale(self, xfactor, yfactor=None, point=None):
-        """
-        Scales the objects' geometry on the XY plane by a given factor.
-        These are:
-
-        * ``buffered_paths``
-        * ``flash_geometry``
-        * ``solid_geometry``
-        * ``regions``
-
-        NOTE:
-        Does not modify the data used to create these elements. If these
-        are recreated, the scaling will be lost. This behavior was modified
-        because of the complexity reached in this class.
-
-        :param xfactor: Number by which to scale on X axis.
-        :type xfactor: float
-        :param yfactor: Number by which to scale on Y axis.
-        :type yfactor: float
-        :rtype : None
-        """
-        log.debug("camlib.Gerber.scale()")
-
-        try:
-            xfactor = float(xfactor)
-        except:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("Scale factor has to be a number: integer or float."))
-            return
-
-        if yfactor is None:
-            yfactor = xfactor
-        else:
-            try:
-                yfactor = float(yfactor)
-            except:
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _("Scale factor has to be a number: integer or float."))
-                return
-
-        if point is None:
-            px = 0
-            py = 0
-        else:
-            px, py = point
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.solid_geometry:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        def scale_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(scale_geom(g))
-                return new_obj
-            else:
-                try:
-                    self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
-                    if self.old_disp_number < disp_number <= 100:
-                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                        self.old_disp_number = disp_number
-
-                    return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
-                except AttributeError:
-                    return obj
-
-        self.solid_geometry = scale_geom(self.solid_geometry)
-        self.follow_geometry = scale_geom(self.follow_geometry)
-
-        # we need to scale the geometry stored in the Gerber apertures, too
-        try:
-            for apid in self.apertures:
-                if 'geometry' in self.apertures[apid]:
-                    for geo_el in self.apertures[apid]['geometry']:
-                        if 'solid' in geo_el:
-                            geo_el['solid'] = scale_geom(geo_el['solid'])
-                        if 'follow' in geo_el:
-                            geo_el['follow'] = scale_geom(geo_el['follow'])
-                        if 'clear' in geo_el:
-                            geo_el['clear'] = scale_geom(geo_el['clear'])
-
-        except Exception as e:
-            log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
-            return 'fail'
-
-        self.app.inform.emit('[success] %s' %
-                             _("Gerber Scale done."))
-        self.app.proc_container.new_text = ''
-
-        # ## solid_geometry ???
-        #  It's a cascaded union of objects.
-        # self.solid_geometry = affinity.scale(self.solid_geometry, factor,
-        #                                      factor, origin=(0, 0))
-
-        # # Now buffered_paths, flash_geometry and solid_geometry
-        # self.create_geometry()
-
-    def offset(self, vect):
-        """
-        Offsets the objects' geometry on the XY plane by a given vector.
-        These are:
-
-        * ``buffered_paths``
-        * ``flash_geometry``
-        * ``solid_geometry``
-        * ``regions``
-
-        NOTE:
-        Does not modify the data used to create these elements. If these
-        are recreated, the scaling will be lost. This behavior was modified
-        because of the complexity reached in this class.
-
-        :param vect: (x, y) offset vector.
-        :type vect: tuple
-        :return: None
-        """
-        log.debug("camlib.Gerber.offset()")
-
-        try:
-            dx, dy = vect
-        except TypeError:
-            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                 _("An (x,y) pair of values are needed. "
-                                   "Probable you entered only one value in the Offset field."))
-            return
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.solid_geometry:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        def offset_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(offset_geom(g))
-                return new_obj
-            else:
-                try:
-                    self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
-                    if self.old_disp_number < disp_number <= 100:
-                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                        self.old_disp_number = disp_number
-
-                    return affinity.translate(obj, xoff=dx, yoff=dy)
-                except AttributeError:
-                    return obj
-
-        # ## Solid geometry
-        self.solid_geometry = offset_geom(self.solid_geometry)
-        self.follow_geometry = offset_geom(self.follow_geometry)
-
-        # we need to offset the geometry stored in the Gerber apertures, too
-        try:
-            for apid in self.apertures:
-                if 'geometry' in self.apertures[apid]:
-                    for geo_el in self.apertures[apid]['geometry']:
-                        if 'solid' in geo_el:
-                            geo_el['solid'] = offset_geom(geo_el['solid'])
-                        if 'follow' in geo_el:
-                            geo_el['follow'] = offset_geom(geo_el['follow'])
-                        if 'clear' in geo_el:
-                            geo_el['clear'] = offset_geom(geo_el['clear'])
-
-        except Exception as e:
-            log.debug('camlib.Gerber.offset() Exception --> %s' % str(e))
-            return 'fail'
-
-        self.app.inform.emit('[success] %s' %
-                             _("Gerber Offset done."))
-        self.app.proc_container.new_text = ''
-
-    def mirror(self, axis, point):
-        """
-        Mirrors the object around a specified axis passing through
-        the given point. What is affected:
-
-        * ``buffered_paths``
-        * ``flash_geometry``
-        * ``solid_geometry``
-        * ``regions``
-
-        NOTE:
-        Does not modify the data used to create these elements. If these
-        are recreated, the scaling will be lost. This behavior was modified
-        because of the complexity reached in this class.
-
-        :param axis: "X" or "Y" indicates around which axis to mirror.
-        :type axis: str
-        :param point: [x, y] point belonging to the mirror axis.
-        :type point: list
-        :return: None
-        """
-        log.debug("camlib.Gerber.mirror()")
-
-        px, py = point
-        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.solid_geometry:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        def mirror_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(mirror_geom(g))
-                return new_obj
-            else:
-                try:
-                    self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
-                    if self.old_disp_number < disp_number <= 100:
-                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                        self.old_disp_number = disp_number
-
-                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
-                except AttributeError:
-                    return obj
-
-        self.solid_geometry = mirror_geom(self.solid_geometry)
-        self.follow_geometry = mirror_geom(self.follow_geometry)
-
-        # we need to mirror the geometry stored in the Gerber apertures, too
-        try:
-            for apid in self.apertures:
-                if 'geometry' in self.apertures[apid]:
-                    for geo_el in self.apertures[apid]['geometry']:
-                        if 'solid' in geo_el:
-                            geo_el['solid'] = mirror_geom(geo_el['solid'])
-                        if 'follow' in geo_el:
-                            geo_el['follow'] = mirror_geom(geo_el['follow'])
-                        if 'clear' in geo_el:
-                            geo_el['clear'] = mirror_geom(geo_el['clear'])
-        except Exception as e:
-            log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e))
-            return 'fail'
-
-        self.app.inform.emit('[success] %s' %
-                             _("Gerber Mirror done."))
-        self.app.proc_container.new_text = ''
-
-    def skew(self, angle_x, angle_y, point):
-        """
-        Shear/Skew the geometries of an object by angles along x and y dimensions.
-
-        Parameters
-        ----------
-        angle_x, angle_y : float, float
-            The shear angle(s) for the x and y axes respectively. These can be
-            specified in either degrees (default) or radians by setting
-            use_radians=True.
-
-        See shapely manual for more information:
-        http://toblerity.org/shapely/manual.html#affine-transformations
-        """
-        log.debug("camlib.Gerber.skew()")
-
-        px, py = point
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.solid_geometry:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        def skew_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(skew_geom(g))
-                return new_obj
-            else:
-                try:
-                    self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                    if self.old_disp_number < disp_number <= 100:
-                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                        self.old_disp_number = disp_number
-
-                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
-                except AttributeError:
-                    return obj
-
-        self.solid_geometry = skew_geom(self.solid_geometry)
-        self.follow_geometry = skew_geom(self.follow_geometry)
-
-        # we need to skew the geometry stored in the Gerber apertures, too
-        try:
-            for apid in self.apertures:
-                if 'geometry' in self.apertures[apid]:
-                    for geo_el in self.apertures[apid]['geometry']:
-                        if 'solid' in geo_el:
-                            geo_el['solid'] = skew_geom(geo_el['solid'])
-                        if 'follow' in geo_el:
-                            geo_el['follow'] = skew_geom(geo_el['follow'])
-                        if 'clear' in geo_el:
-                            geo_el['clear'] = skew_geom(geo_el['clear'])
-        except Exception as e:
-            log.debug('camlib.Gerber.skew() Exception --> %s' % str(e))
-            return 'fail'
-
-        self.app.inform.emit('[success] %s' %
-                             _("Gerber Skew done."))
-        self.app.proc_container.new_text = ''
-
-    def rotate(self, angle, point):
-        """
-        Rotate an object by a given angle around given coords (point)
-        :param angle:
-        :param point:
-        :return:
-        """
-        log.debug("camlib.Gerber.rotate()")
-
-        px, py = point
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.solid_geometry:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        def rotate_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(rotate_geom(g))
-                return new_obj
-            else:
-                try:
-                    self.el_count += 1
-                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                    if self.old_disp_number < disp_number <= 100:
-                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                        self.old_disp_number = disp_number
-
-                    return affinity.rotate(obj, angle, origin=(px, py))
-                except AttributeError:
-                    return obj
-
-        self.solid_geometry = rotate_geom(self.solid_geometry)
-        self.follow_geometry = rotate_geom(self.follow_geometry)
-
-        # we need to rotate the geometry stored in the Gerber apertures, too
-        try:
-            for apid in self.apertures:
-                if 'geometry' in self.apertures[apid]:
-                    for geo_el in self.apertures[apid]['geometry']:
-                        if 'solid' in geo_el:
-                            geo_el['solid'] = rotate_geom(geo_el['solid'])
-                        if 'follow' in geo_el:
-                            geo_el['follow'] = rotate_geom(geo_el['follow'])
-                        if 'clear' in geo_el:
-                            geo_el['clear'] = rotate_geom(geo_el['clear'])
-        except Exception as e:
-            log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
-            return 'fail'
-        self.app.inform.emit('[success] %s' %
-                             _("Gerber Rotate done."))
-        self.app.proc_container.new_text = ''
-
-
-class Excellon(Geometry):
-    """
-    Here it is done all the Excellon parsing.
-
-    *ATTRIBUTES*
-
-    * ``tools`` (dict): The key is the tool name and the value is
-      a dictionary specifying the tool:
-
-    ================  ====================================
-    Key               Value
-    ================  ====================================
-    C                 Diameter of the tool
-    solid_geometry    Geometry list for each tool
-    Others            Not supported (Ignored).
-    ================  ====================================
-
-    * ``drills`` (list): Each is a dictionary:
-
-    ================  ====================================
-    Key               Value
-    ================  ====================================
-    point             (Shapely.Point) Where to drill
-    tool              (str) A key in ``tools``
-    ================  ====================================
-
-    * ``slots`` (list): Each is a dictionary
-
-    ================  ====================================
-    Key               Value
-    ================  ====================================
-    start             (Shapely.Point) Start point of the slot
-    stop              (Shapely.Point) Stop point of the slot
-    tool              (str) A key in ``tools``
-    ================  ====================================
-    """
-
-    defaults = {
-        "zeros": "L",
-        "excellon_format_upper_mm": '3',
-        "excellon_format_lower_mm": '3',
-        "excellon_format_upper_in": '2',
-        "excellon_format_lower_in": '4',
-        "excellon_units": 'INCH',
-        "geo_steps_per_circle": '64'
-    }
-
-    def __init__(self, zeros=None, excellon_format_upper_mm=None, excellon_format_lower_mm=None,
-                 excellon_format_upper_in=None, excellon_format_lower_in=None, excellon_units=None,
-                 geo_steps_per_circle=None):
-        """
-        The constructor takes no parameters.
-
-        :return: Excellon object.
-        :rtype: Excellon
-        """
-
-        if geo_steps_per_circle is None:
-            geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle'])
-        self.geo_steps_per_circle = int(geo_steps_per_circle)
-
-        Geometry.__init__(self, geo_steps_per_circle=int(geo_steps_per_circle))
-
-        # dictionary to store tools, see above for description
-        self.tools = {}
-        # list to store the drills, see above for description
-        self.drills = []
-
-        # self.slots (list) to store the slots; each is a dictionary
-        self.slots = []
-
-        self.source_file = ''
-
-        # it serve to flag if a start routing or a stop routing was encountered
-        # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error
-        self.routing_flag = 1
-
-        self.match_routing_start = None
-        self.match_routing_stop = None
-
-        self.num_tools = []  # List for keeping the tools sorted
-        self.index_per_tool = {}  # Dictionary to store the indexed points for each tool
-
-        # ## IN|MM -> Units are inherited from Geometry
-        # self.units = units
-
-        # Trailing "T" or leading "L" (default)
-        # self.zeros = "T"
-        self.zeros = zeros or self.defaults["zeros"]
-        self.zeros_found = self.zeros
-        self.units_found = self.units
-
-        # this will serve as a default if the Excellon file has no info regarding of tool diameters (this info may be
-        # in another file like for PCB WIzard ECAD software
-        self.toolless_diam = 1.0
-        # signal that the Excellon file has no tool diameter informations and the tools have bogus (random) diameter
-        self.diameterless = False
-
-        # Excellon format
-        self.excellon_format_upper_in = excellon_format_upper_in or self.defaults["excellon_format_upper_in"]
-        self.excellon_format_lower_in = excellon_format_lower_in or self.defaults["excellon_format_lower_in"]
-        self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"]
-        self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"]
-        self.excellon_units = excellon_units or self.defaults["excellon_units"]
-        # detected Excellon format is stored here:
-        self.excellon_format = None
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from Geometry.
-        self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
-                           'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots',
-                           'source_file']
-
-        # ### Patterns ####
-        # Regex basics:
-        # ^ - beginning
-        # $ - end
-        # *: 0 or more, +: 1 or more, ?: 0 or 1
-
-        # M48 - Beginning of Part Program Header
-        self.hbegin_re = re.compile(r'^M48$')
-
-        # ;HEADER - Beginning of Allegro Program Header
-        self.allegro_hbegin_re = re.compile(r'\;\s*(HEADER)')
-
-        # M95 or % - End of Part Program Header
-        # NOTE: % has different meaning in the body
-        self.hend_re = re.compile(r'^(?:M95|%)$')
-
-        # FMAT Excellon format
-        # Ignored in the parser
-        #self.fmat_re = re.compile(r'^FMAT,([12])$')
-
-        # Uunits and possible Excellon zeros and possible Excellon format
-        # INCH uses 6 digits
-        # METRIC uses 5/6
-        self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?,?(\d*\.\d+)?.*$')
-
-        # Tool definition/parameters (?= is look-ahead
-        # NOTE: This might be an overkill!
-        # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
-        #                              r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
-        #                              r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
-        #                              r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
-        self.toolset_re = re.compile(r'^T(\d+)(?=.*C,?(\d*\.?\d*))?' +
-                                     r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
-                                     r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
-                                     r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
-
-        self.detect_gcode_re = re.compile(r'^G2([01])$')
-
-        # Tool select
-        # Can have additional data after tool number but
-        # is ignored if present in the header.
-        # Warning: This will match toolset_re too.
-        # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
-        self.toolsel_re = re.compile(r'^T(\d+)')
-
-        # Headerless toolset
-        # self.toolset_hl_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))')
-        self.toolset_hl_re = re.compile(r'^T(\d+)(?:.?C(\d+\.?\d*))?')
-
-        # Comment
-        self.comm_re = re.compile(r'^;(.*)$')
-
-        # Absolute/Incremental G90/G91
-        self.absinc_re = re.compile(r'^G9([01])$')
-
-        # Modes of operation
-        # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
-        self.modes_re = re.compile(r'^G0([012345])')
-
-        # Measuring mode
-        # 1-metric, 2-inch
-        self.meas_re = re.compile(r'^M7([12])$')
-
-        # Coordinates
-        # self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
-        # self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
-        coordsperiod_re_string = r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]'
-        self.coordsperiod_re = re.compile(coordsperiod_re_string)
-
-        coordsnoperiod_re_string = r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]'
-        self.coordsnoperiod_re = re.compile(coordsnoperiod_re_string)
-
-        # Slots parsing
-        slots_re_string = r'^([^G]+)G85(.*)$'
-        self.slots_re = re.compile(slots_re_string)
-
-        # R - Repeat hole (# times, X offset, Y offset)
-        self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$')
-
-        # Various stop/pause commands
-        self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
-
-        # Allegro Excellon format support
-        self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))')
-
-        # Altium Excellon format support
-        # it's a comment like this: ";FILE_FORMAT=2:5"
-        self.altium_format = re.compile(r'^;\s*(?:FILE_FORMAT)?(?:Format)?[=|:]\s*(\d+)[:|.](\d+).*$')
-
-        # Parse coordinates
-        self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)')
-
-        # Repeating command
-        self.repeat_re = re.compile(r'R(\d+)')
-
-    def parse_file(self, filename=None, file_obj=None):
-        """
-        Reads the specified file as array of lines as
-        passes it to ``parse_lines()``.
-
-        :param filename: The file to be read and parsed.
-        :type filename: str
-        :return: None
-        """
-        if file_obj:
-            estr = file_obj
-        else:
-            if filename is None:
-                return "fail"
-            efile = open(filename, 'r')
-            estr = efile.readlines()
-            efile.close()
-
-        try:
-            self.parse_lines(estr)
-        except:
-            return "fail"
-
-    def parse_lines(self, elines):
-        """
-        Main Excellon parser.
-
-        :param elines: List of strings, each being a line of Excellon code.
-        :type elines: list
-        :return: None
-        """
-
-        # State variables
-        current_tool = ""
-        in_header = False
-        headerless = False
-        current_x = None
-        current_y = None
-
-        slot_current_x = None
-        slot_current_y = None
-
-        name_tool = 0
-        allegro_warning = False
-        line_units_found = False
-
-        repeating_x = 0
-        repeating_y = 0
-        repeat = 0
-
-        line_units = ''
-
-        #### Parsing starts here ## ##
-        line_num = 0  # Line number
-        eline = ""
-        try:
-            for eline in elines:
-                if self.app.abort_flag:
-                    # graceful abort requested by the user
-                    raise FlatCAMApp.GracefulException
-
-                line_num += 1
-                # log.debug("%3d %s" % (line_num, str(eline)))
-
-                self.source_file += eline
-
-                # Cleanup lines
-                eline = eline.strip(' \r\n')
-
-                # Excellon files and Gcode share some extensions therefore if we detect G20 or G21 it's GCODe
-                # and we need to exit from here
-                if self.detect_gcode_re.search(eline):
-                    log.warning("This is GCODE mark: %s" % eline)
-                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                         (_('This is GCODE mark'), eline))
-                    return
-
-                # Header Begin (M48) #
-                if self.hbegin_re.search(eline):
-                    in_header = True
-                    headerless = False
-                    log.warning("Found start of the header: %s" % eline)
-                    continue
-
-                # Allegro Header Begin (;HEADER) #
-                if self.allegro_hbegin_re.search(eline):
-                    in_header = True
-                    allegro_warning = True
-                    log.warning("Found ALLEGRO start of the header: %s" % eline)
-                    continue
-
-                # Search for Header End #
-                # Since there might be comments in the header that include header end char (% or M95)
-                # we ignore the lines starting with ';' that contains such header end chars because it is not a
-                # real header end.
-                if self.comm_re.search(eline):
-                    match = self.tool_units_re.search(eline)
-                    if match:
-                        if line_units_found is False:
-                            line_units_found = True
-                            line_units = match.group(3)
-                            self.convert_units({"MILS": "IN", "MM": "MM"}[line_units])
-                            log.warning("Type of Allegro UNITS found inline in comments: %s" % line_units)
-
-                        if match.group(2):
-                            name_tool += 1
-                            if line_units == 'MILS':
-                                spec = {"C": (float(match.group(2)) / 1000)}
-                                self.tools[str(name_tool)] = spec
-                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
-                            else:
-                                spec = {"C": float(match.group(2))}
-                                self.tools[str(name_tool)] = spec
-                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
-                            spec['solid_geometry'] = []
-                            continue
-                    # search for Altium Excellon Format / Sprint Layout who is included as a comment
-                    match = self.altium_format.search(eline)
-                    if match:
-                        self.excellon_format_upper_mm = match.group(1)
-                        self.excellon_format_lower_mm = match.group(2)
-
-                        self.excellon_format_upper_in = match.group(1)
-                        self.excellon_format_lower_in = match.group(2)
-                        log.warning("Altium Excellon format preset found in comments: %s:%s" %
-                                    (match.group(1), match.group(2)))
-                        continue
-                    else:
-                        log.warning("Line ignored, it's a comment: %s" % eline)
-                else:
-                    if self.hend_re.search(eline):
-                        if in_header is False or bool(self.tools) is False:
-                            log.warning("Found end of the header but there is no header: %s" % eline)
-                            log.warning("The only useful data in header are tools, units and format.")
-                            log.warning("Therefore we will create units and format based on defaults.")
-                            headerless = True
-                            try:
-                                self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.excellon_units])
-                            except Exception as e:
-                                log.warning("Units could not be converted: %s" % str(e))
-
-                        in_header = False
-                        # for Allegro type of Excellons we reset name_tool variable so we can reuse it for toolchange
-                        if allegro_warning is True:
-                            name_tool = 0
-                        log.warning("Found end of the header: %s" % eline)
-                        continue
-
-                # ## Alternative units format M71/M72
-                # Supposed to be just in the body (yes, the body)
-                # but some put it in the header (PADS for example).
-                # Will detect anywhere. Occurrence will change the
-                # object's units.
-                match = self.meas_re.match(eline)
-                if match:
-                    # self.units = {"1": "MM", "2": "IN"}[match.group(1)]
-
-                    # Modified for issue #80
-                    self.convert_units({"1": "MM", "2": "IN"}[match.group(1)])
-                    log.debug("  Units: %s" % self.units)
-                    if self.units == 'MM':
-                        log.warning("Excellon format preset is: %s" % self.excellon_format_upper_mm + \
-                                    ':' + str(self.excellon_format_lower_mm))
-                    else:
-                        log.warning("Excellon format preset is: %s" % self.excellon_format_upper_in + \
-                        ':' + str(self.excellon_format_lower_in))
-                    continue
-
-                # ### Body ####
-                if not in_header:
-
-                    # ## Tool change ###
-                    match = self.toolsel_re.search(eline)
-                    if match:
-                        current_tool = str(int(match.group(1)))
-                        log.debug("Tool change: %s" % current_tool)
-                        if bool(headerless):
-                            match = self.toolset_hl_re.search(eline)
-                            if match:
-                                name = str(int(match.group(1)))
-                                try:
-                                    diam = float(match.group(2))
-                                except:
-                                    # it's possible that tool definition has only tool number and no diameter info
-                                    # (those could be in another file like PCB Wizard do)
-                                    # then match.group(2) = None and float(None) will create the exception
-                                    # the bellow construction is so each tool will have a slightly different diameter
-                                    # starting with a default value, to allow Excellon editing after that
-                                    self.diameterless = True
-                                    self.app.inform.emit('[WARNING] %s%s %s' %
-                                                         (_("No tool diameter info's. See shell.\n"
-                                                            "A tool change event: T"),
-                                                          str(current_tool),
-                                                          _("was found but the Excellon file "
-                                                            "have no informations regarding the tool "
-                                                            "diameters therefore the application will try to load it "
-                                                            "by using some 'fake' diameters.\n"
-                                                            "The user needs to edit the resulting Excellon object and "
-                                                            "change the diameters to reflect the real diameters.")
-                                                          )
-                                                         )
-
-                                    if self.excellon_units == 'MM':
-                                        diam = self.toolless_diam + (int(current_tool) - 1) / 100
-                                    else:
-                                        diam = (self.toolless_diam + (int(current_tool) - 1) / 100) / 25.4
-
-                                spec = {"C": diam, 'solid_geometry': []}
-                                self.tools[name] = spec
-                                log.debug("Tool definition out of header: %s %s" % (name, spec))
-
-                        continue
-
-                    # ## Allegro Type Tool change ###
-                    if allegro_warning is True:
-                        match = self.absinc_re.search(eline)
-                        match1 = self.stop_re.search(eline)
-                        if match or match1:
-                            name_tool += 1
-                            current_tool = str(name_tool)
-                            log.debug("Tool change for Allegro type of Excellon: %s" % current_tool)
-                            continue
-
-                    # ## Slots parsing for drilled slots (contain G85)
-                    # a Excellon drilled slot line may look like this:
-                    # X01125Y0022244G85Y0027756
-                    match = self.slots_re.search(eline)
-                    if match:
-                        # signal that there are milling slots operations
-                        self.defaults['excellon_drills'] = False
-
-                        # the slot start coordinates group is to the left of G85 command (group(1) )
-                        # the slot stop coordinates group is to the right of G85 command (group(2) )
-                        start_coords_match = match.group(1)
-                        stop_coords_match = match.group(2)
-
-                        # Slot coordinates without period # ##
-                        # get the coordinates for slot start and for slot stop into variables
-                        start_coords_noperiod = self.coordsnoperiod_re.search(start_coords_match)
-                        stop_coords_noperiod = self.coordsnoperiod_re.search(stop_coords_match)
-                        if start_coords_noperiod:
-                            try:
-                                slot_start_x = self.parse_number(start_coords_noperiod.group(1))
-                                slot_current_x = slot_start_x
-                            except TypeError:
-                                slot_start_x = slot_current_x
-                            except:
-                                return
-
-                            try:
-                                slot_start_y = self.parse_number(start_coords_noperiod.group(2))
-                                slot_current_y = slot_start_y
-                            except TypeError:
-                                slot_start_y = slot_current_y
-                            except:
-                                return
-
-                            try:
-                                slot_stop_x = self.parse_number(stop_coords_noperiod.group(1))
-                                slot_current_x = slot_stop_x
-                            except TypeError:
-                                slot_stop_x = slot_current_x
-                            except:
-                                return
-
-                            try:
-                                slot_stop_y = self.parse_number(stop_coords_noperiod.group(2))
-                                slot_current_y = slot_stop_y
-                            except TypeError:
-                                slot_stop_y = slot_current_y
-                            except:
-                                return
-
-                            if (slot_start_x is None or slot_start_y is None or
-                                                slot_stop_x is None or slot_stop_y is None):
-                                log.error("Slots are missing some or all coordinates.")
-                                continue
-
-                            # we have a slot
-                            log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
-                                                                               slot_start_y, slot_stop_x,
-                                                                               slot_stop_y]))
-
-                            # store current tool diameter as slot diameter
-                            slot_dia = 0.05
-                            try:
-                                slot_dia = float(self.tools[current_tool]['C'])
-                            except Exception as e:
-                                pass
-                            log.debug(
-                                'Milling/Drilling slot with tool %s, diam=%f' % (
-                                    current_tool,
-                                    slot_dia
-                                )
-                            )
-
-                            self.slots.append(
-                                {
-                                    'start': Point(slot_start_x, slot_start_y),
-                                    'stop': Point(slot_stop_x, slot_stop_y),
-                                    'tool': current_tool
-                                }
-                            )
-                            continue
-
-                        # Slot coordinates with period: Use literally. ###
-                        # get the coordinates for slot start and for slot stop into variables
-                        start_coords_period = self.coordsperiod_re.search(start_coords_match)
-                        stop_coords_period = self.coordsperiod_re.search(stop_coords_match)
-                        if start_coords_period:
-
-                            try:
-                                slot_start_x = float(start_coords_period.group(1))
-                                slot_current_x = slot_start_x
-                            except TypeError:
-                                slot_start_x = slot_current_x
-                            except:
-                                return
-
-                            try:
-                                slot_start_y = float(start_coords_period.group(2))
-                                slot_current_y = slot_start_y
-                            except TypeError:
-                                slot_start_y = slot_current_y
-                            except:
-                                return
-
-                            try:
-                                slot_stop_x = float(stop_coords_period.group(1))
-                                slot_current_x = slot_stop_x
-                            except TypeError:
-                                slot_stop_x = slot_current_x
-                            except:
-                                return
-
-                            try:
-                                slot_stop_y = float(stop_coords_period.group(2))
-                                slot_current_y = slot_stop_y
-                            except TypeError:
-                                slot_stop_y = slot_current_y
-                            except:
-                                return
-
-                            if (slot_start_x is None or slot_start_y is None or
-                                                slot_stop_x is None or slot_stop_y is None):
-                                log.error("Slots are missing some or all coordinates.")
-                                continue
-
-                            # we have a slot
-                            log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
-                                                                            slot_start_y, slot_stop_x, slot_stop_y]))
-
-                            # store current tool diameter as slot diameter
-                            slot_dia = 0.05
-                            try:
-                                slot_dia = float(self.tools[current_tool]['C'])
-                            except Exception as e:
-                                pass
-                            log.debug(
-                                'Milling/Drilling slot with tool %s, diam=%f' % (
-                                    current_tool,
-                                    slot_dia
-                                )
-                            )
-
-                            self.slots.append(
-                                {
-                                    'start': Point(slot_start_x, slot_start_y),
-                                    'stop': Point(slot_stop_x, slot_stop_y),
-                                    'tool': current_tool
-                                }
-                            )
-                        continue
-
-                    # ## Coordinates without period # ##
-                    match = self.coordsnoperiod_re.search(eline)
-                    if match:
-                        matchr = self.repeat_re.search(eline)
-                        if matchr:
-                            repeat = int(matchr.group(1))
-
-                        try:
-                            x = self.parse_number(match.group(1))
-                            repeating_x = current_x
-                            current_x = x
-                        except TypeError:
-                            x = current_x
-                            repeating_x = 0
-                        except:
-                            return
-
-                        try:
-                            y = self.parse_number(match.group(2))
-                            repeating_y = current_y
-                            current_y = y
-                        except TypeError:
-                            y = current_y
-                            repeating_y = 0
-                        except:
-                            return
-
-                        if x is None or y is None:
-                            log.error("Missing coordinates")
-                            continue
-
-                        # ## Excellon Routing parse
-                        if len(re.findall("G00", eline)) > 0:
-                            self.match_routing_start = 'G00'
-
-                            # signal that there are milling slots operations
-                            self.defaults['excellon_drills'] = False
-
-                            self.routing_flag = 0
-                            slot_start_x = x
-                            slot_start_y = y
-                            continue
-
-                        if self.routing_flag == 0:
-                            if len(re.findall("G01", eline)) > 0:
-                                self.match_routing_stop = 'G01'
-
-                                # signal that there are milling slots operations
-                                self.defaults['excellon_drills'] = False
-
-                                self.routing_flag = 1
-                                slot_stop_x = x
-                                slot_stop_y = y
-                                self.slots.append(
-                                    {
-                                        'start': Point(slot_start_x, slot_start_y),
-                                        'stop': Point(slot_stop_x, slot_stop_y),
-                                        'tool': current_tool
-                                    }
-                                )
-                                continue
-
-                        if self.match_routing_start is None and self.match_routing_stop is None:
-                            if repeat == 0:
-                                # signal that there are drill operations
-                                self.defaults['excellon_drills'] = True
-                                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
-                            else:
-                                coordx = x
-                                coordy = y
-                                while repeat > 0:
-                                    if repeating_x:
-                                        coordx = (repeat * x) + repeating_x
-                                    if repeating_y:
-                                        coordy = (repeat * y) + repeating_y
-                                    self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
-                                    repeat -= 1
-                            repeating_x = repeating_y = 0
-                            # log.debug("{:15} {:8} {:8}".format(eline, x, y))
-                            continue
-
-                    # ## Coordinates with period: Use literally. # ##
-                    match = self.coordsperiod_re.search(eline)
-                    if match:
-                        matchr = self.repeat_re.search(eline)
-                        if matchr:
-                            repeat = int(matchr.group(1))
-
-                    if match:
-                        # signal that there are drill operations
-                        self.defaults['excellon_drills'] = True
-                        try:
-                            x = float(match.group(1))
-                            repeating_x = current_x
-                            current_x = x
-                        except TypeError:
-                            x = current_x
-                            repeating_x = 0
-
-                        try:
-                            y = float(match.group(2))
-                            repeating_y = current_y
-                            current_y = y
-                        except TypeError:
-                            y = current_y
-                            repeating_y = 0
-
-                        if x is None or y is None:
-                            log.error("Missing coordinates")
-                            continue
-
-                        # ## Excellon Routing parse
-                        if len(re.findall("G00", eline)) > 0:
-                            self.match_routing_start = 'G00'
-
-                            # signal that there are milling slots operations
-                            self.defaults['excellon_drills'] = False
-
-                            self.routing_flag = 0
-                            slot_start_x = x
-                            slot_start_y = y
-                            continue
-
-                        if self.routing_flag == 0:
-                            if len(re.findall("G01", eline)) > 0:
-                                self.match_routing_stop = 'G01'
-
-                                # signal that there are milling slots operations
-                                self.defaults['excellon_drills'] = False
-
-                                self.routing_flag = 1
-                                slot_stop_x = x
-                                slot_stop_y = y
-                                self.slots.append(
-                                    {
-                                        'start': Point(slot_start_x, slot_start_y),
-                                        'stop': Point(slot_stop_x, slot_stop_y),
-                                        'tool': current_tool
-                                    }
-                                )
-                                continue
-
-                        if self.match_routing_start is None and self.match_routing_stop is None:
-                            # signal that there are drill operations
-                            if repeat == 0:
-                                # signal that there are drill operations
-                                self.defaults['excellon_drills'] = True
-                                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
-                            else:
-                                coordx = x
-                                coordy = y
-                                while repeat > 0:
-                                    if repeating_x:
-                                        coordx = (repeat * x) + repeating_x
-                                    if repeating_y:
-                                        coordy = (repeat * y) + repeating_y
-                                    self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
-                                    repeat -= 1
-                            repeating_x = repeating_y = 0
-                            # log.debug("{:15} {:8} {:8}".format(eline, x, y))
-                            continue
-
-                # ### Header ####
-                if in_header:
-
-                    # ## Tool definitions # ##
-                    match = self.toolset_re.search(eline)
-                    if match:
-
-                        name = str(int(match.group(1)))
-                        spec = {"C": float(match.group(2)), 'solid_geometry': []}
-                        self.tools[name] = spec
-                        log.debug("  Tool definition: %s %s" % (name, spec))
-                        continue
-
-                    # ## Units and number format # ##
-                    match = self.units_re.match(eline)
-                    if match:
-                        self.units_found = match.group(1)
-                        self.zeros = match.group(2)  # "T" or "L". Might be empty
-                        self.excellon_format = match.group(3)
-                        if self.excellon_format:
-                            upper = len(self.excellon_format.partition('.')[0])
-                            lower = len(self.excellon_format.partition('.')[2])
-                            if self.units == 'MM':
-                                self.excellon_format_upper_mm = upper
-                                self.excellon_format_lower_mm = lower
-                            else:
-                                self.excellon_format_upper_in = upper
-                                self.excellon_format_lower_in = lower
-
-                        # Modified for issue #80
-                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
-                        # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
-                        log.warning("Units: %s" % self.units)
-                        if self.units == 'MM':
-                            log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
-                                        ':' + str(self.excellon_format_lower_mm))
-                        else:
-                            log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
-                                        ':' + str(self.excellon_format_lower_in))
-                        log.warning("Type of zeros found inline: %s" % self.zeros)
-                        continue
-
-                    # Search for units type again it might be alone on the line
-                    if "INCH" in eline:
-                        line_units = "INCH"
-                        # Modified for issue #80
-                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
-                        log.warning("Type of UNITS found inline: %s" % line_units)
-                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
-                                    ':' + str(self.excellon_format_lower_in))
-                        # TODO: not working
-                        #FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
-                        continue
-                    elif "METRIC" in eline:
-                        line_units = "METRIC"
-                        # Modified for issue #80
-                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
-                        log.warning("Type of UNITS found inline: %s" % line_units)
-                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
-                                    ':' + str(self.excellon_format_lower_mm))
-                        # TODO: not working
-                        #FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
-                        continue
-
-                    # Search for zeros type again because it might be alone on the line
-                    match = re.search(r'[LT]Z',eline)
-                    if match:
-                        self.zeros = match.group()
-                        log.warning("Type of zeros found: %s" % self.zeros)
-                        continue
-
-                # ## Units and number format outside header# ##
-                match = self.units_re.match(eline)
-                if match:
-                    self.units_found = match.group(1)
-                    self.zeros = match.group(2)  # "T" or "L". Might be empty
-                    self.excellon_format = match.group(3)
-                    if self.excellon_format:
-                        upper = len(self.excellon_format.partition('.')[0])
-                        lower = len(self.excellon_format.partition('.')[2])
-                        if self.units == 'MM':
-                            self.excellon_format_upper_mm = upper
-                            self.excellon_format_lower_mm = lower
-                        else:
-                            self.excellon_format_upper_in = upper
-                            self.excellon_format_lower_in = lower
-
-                    # Modified for issue #80
-                    self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
-                    # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
-                    log.warning("Units: %s" % self.units)
-                    if self.units == 'MM':
-                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
-                                    ':' + str(self.excellon_format_lower_mm))
-                    else:
-                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
-                                    ':' + str(self.excellon_format_lower_in))
-                    log.warning("Type of zeros found outside header, inline: %s" % self.zeros)
-
-                    log.warning("UNITS found outside header")
-                    continue
-
-                log.warning("Line ignored: %s" % eline)
-
-            # make sure that since we are in headerless mode, we convert the tools only after the file parsing
-            # is finished since the tools definitions are spread in the Excellon body. We use as units the value
-            # from self.defaults['excellon_units']
-            log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
-        except Exception as e:
-            log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
-            msg = '[ERROR_NOTCL] %s' % \
-                  _("An internal error has ocurred. See shell.\n")
-            msg += _('{e_code} Excellon Parser error.\nParsing Failed. Line {l_nr}: {line}\n').format(
-                e_code='[ERROR]',
-                l_nr=line_num,
-                line=eline)
-            msg += traceback.format_exc()
-            self.app.inform.emit(msg)
-
-            return "fail"
-        
-    def parse_number(self, number_str):
-        """
-        Parses coordinate numbers without period.
-
-        :param number_str: String representing the numerical value.
-        :type number_str: str
-        :return: Floating point representation of the number
-        :rtype: float
-        """
-
-        match = self.leadingzeros_re.search(number_str)
-        nr_length = len(match.group(1)) + len(match.group(2))
-        try:
-            if self.zeros == "L" or self.zeros == "LZ": # Leading
-                # With leading zeros, when you type in a coordinate,
-                # the leading zeros must always be included.  Trailing zeros
-                # are unneeded and may be left off. The CNC-7 will automatically add them.
-                # r'^[-\+]?(0*)(\d*)'
-                # 6 digits are divided by 10^4
-                # If less than size digits, they are automatically added,
-                # 5 digits then are divided by 10^3 and so on.
-
-                if self.units.lower() == "in":
-                    result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_in)))
-                else:
-                    result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_mm)))
-                return result
-            else:  # Trailing
-                # You must show all zeros to the right of the number and can omit
-                # all zeros to the left of the number. The CNC-7 will count the number
-                # of digits you typed and automatically fill in the missing zeros.
-                # ## flatCAM expects 6digits
-                # flatCAM expects the number of digits entered into the defaults
-
-                if self.units.lower() == "in":  # Inches is 00.0000
-                    result = float(number_str) / (10 ** (float(self.excellon_format_lower_in)))
-                else:   # Metric is 000.000
-                    result = float(number_str) / (10 ** (float(self.excellon_format_lower_mm)))
-                return result
-        except Exception as e:
-            log.error("Aborted. Operation could not be completed due of %s" % str(e))
-            return
-
-    def create_geometry(self):
-        """
-        Creates circles of the tool diameter at every point
-        specified in ``self.drills``. Also creates geometries (polygons)
-        for the slots as specified in ``self.slots``
-        All the resulting geometry is stored into self.solid_geometry list.
-        The list self.solid_geometry has 2 elements: first is a dict with the drills geometry,
-        and second element is another similar dict that contain the slots geometry.
-
-        Each dict has as keys the tool diameters and as values lists with Shapely objects, the geometries
-        ================  ====================================
-        Key               Value
-        ================  ====================================
-        tool_diameter     list of (Shapely.Point) Where to drill
-        ================  ====================================
-
-        :return: None
-        """
-        self.solid_geometry = []
-        try:
-            # clear the solid_geometry in self.tools
-            for tool in self.tools:
-                try:
-                    self.tools[tool]['solid_geometry'][:] = []
-                except KeyError:
-                    self.tools[tool]['solid_geometry'] = []
-
-            for drill in self.drills:
-                # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
-                if drill['tool'] is '':
-                    self.app.inform.emit('[WARNING] %s' %
-                                         _("Excellon.create_geometry() -> a drill location was skipped "
-                                           "due of not having a tool associated.\n"
-                                           "Check the resulting GCode."))
-                    log.debug("Excellon.create_geometry() -> a drill location was skipped "
-                              "due of not having a tool associated")
-                    continue
-                tooldia = self.tools[drill['tool']]['C']
-                poly = drill['point'].buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
-                self.solid_geometry.append(poly)
-                self.tools[drill['tool']]['solid_geometry'].append(poly)
-
-            for slot in self.slots:
-                slot_tooldia = self.tools[slot['tool']]['C']
-                start = slot['start']
-                stop = slot['stop']
-
-                lines_string = LineString([start, stop])
-                poly = lines_string.buffer(slot_tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
-                self.solid_geometry.append(poly)
-                self.tools[slot['tool']]['solid_geometry'].append(poly)
-
-        except Exception as e:
-            log.debug("Excellon geometry creation failed due of ERROR: %s" % str(e))
-            return "fail"
-
-        # drill_geometry = {}
-        # slot_geometry = {}
-        #
-        # def insertIntoDataStruct(dia, drill_geo, aDict):
-        #     if not dia in aDict:
-        #         aDict[dia] = [drill_geo]
-        #     else:
-        #         aDict[dia].append(drill_geo)
-        #
-        # for tool in self.tools:
-        #     tooldia = self.tools[tool]['C']
-        #     for drill in self.drills:
-        #         if drill['tool'] == tool:
-        #             poly = drill['point'].buffer(tooldia / 2.0)
-        #             insertIntoDataStruct(tooldia, poly, drill_geometry)
-        #
-        # for tool in self.tools:
-        #     slot_tooldia = self.tools[tool]['C']
-        #     for slot in self.slots:
-        #         if slot['tool'] == tool:
-        #             start = slot['start']
-        #             stop = slot['stop']
-        #             lines_string = LineString([start, stop])
-        #             poly = lines_string.buffer(slot_tooldia/2.0, self.geo_steps_per_circle)
-        #             insertIntoDataStruct(slot_tooldia, poly, drill_geometry)
-        #
-        # self.solid_geometry = [drill_geometry, slot_geometry]
-
-    def bounds(self):
-        """
-        Returns coordinates of rectangular bounds
-        of Excellon geometry: (xmin, ymin, xmax, ymax).
-        """
-        # fixed issue of getting bounds only for one level lists of objects
-        # now it can get bounds for nested lists of objects
-
-        log.debug("camlib.Excellon.bounds()")
-        if self.solid_geometry is None:
-            log.debug("solid_geometry is None")
-            return 0, 0, 0, 0
-
-        def bounds_rec(obj):
-            if type(obj) is list:
-                minx = Inf
-                miny = Inf
-                maxx = -Inf
-                maxy = -Inf
-
-                for k in obj:
-                    if type(k) is dict:
-                        for key in k:
-                            minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
-                            minx = min(minx, minx_)
-                            miny = min(miny, miny_)
-                            maxx = max(maxx, maxx_)
-                            maxy = max(maxy, maxy_)
-                    else:
-                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
-                        minx = min(minx, minx_)
-                        miny = min(miny, miny_)
-                        maxx = max(maxx, maxx_)
-                        maxy = max(maxy, maxy_)
-                return minx, miny, maxx, maxy
-            else:
-                # it's a Shapely object, return it's bounds
-                return obj.bounds
-
-        minx_list = []
-        miny_list = []
-        maxx_list = []
-        maxy_list = []
-
-        for tool in self.tools:
-            minx, miny, maxx, maxy = bounds_rec(self.tools[tool]['solid_geometry'])
-            minx_list.append(minx)
-            miny_list.append(miny)
-            maxx_list.append(maxx)
-            maxy_list.append(maxy)
-
-        return (min(minx_list), min(miny_list), max(maxx_list), max(maxy_list))
-
-    def convert_units(self, units):
-        """
-        This function first convert to the the units found in the Excellon file but it converts tools that
-        are not there yet so it has no effect other than it signal that the units are the ones in the file.
-
-        On object creation, in new_object(), true conversion is done because this is done at the end of the
-        Excellon file parsing, the tools are inside and self.tools is really converted from the units found
-        inside the file to the FlatCAM units.
-
-        Kind of convolute way to make the conversion and it is based on the assumption that the Excellon file
-        will have detected the units before the tools are parsed and stored in self.tools
-        :param units:
-        :type str: IN or MM
-        :return:
-        """
-        log.debug("camlib.Excellon.convert_units()")
-
-        factor = Geometry.convert_units(self, units)
-
-        # Tools
-        for tname in self.tools:
-            self.tools[tname]["C"] *= factor
-
-        self.create_geometry()
-
-        return factor
-
-    def scale(self, xfactor, yfactor=None, point=None):
-        """
-        Scales geometry on the XY plane in the object by a given factor.
-        Tool sizes, feedrates an Z-plane dimensions are untouched.
-
-        :param factor: Number by which to scale the object.
-        :type factor: float
-        :return: None
-        :rtype: NOne
-        """
-        log.debug("camlib.Excellon.scale()")
-
-        if yfactor is None:
-            yfactor = xfactor
-
-        if point is None:
-            px = 0
-            py = 0
-        else:
-            px, py = point
-
-        def scale_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(scale_geom(g))
-                return new_obj
-            else:
-                try:
-                    return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
-                except AttributeError:
-                    return obj
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.drills:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        # Drills
-        for drill in self.drills:
-            drill['point'] = affinity.scale(drill['point'], xfactor, yfactor, origin=(px, py))
-
-            self.el_count += 1
-            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-            if self.old_disp_number < disp_number <= 100:
-                self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                self.old_disp_number = disp_number
-
-        # scale solid_geometry
-        for tool in self.tools:
-            self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry'])
-
-        # Slots
-        for slot in self.slots:
-            slot['stop'] = affinity.scale(slot['stop'], xfactor, yfactor, origin=(px, py))
-            slot['start'] = affinity.scale(slot['start'], xfactor, yfactor, origin=(px, py))
-
-        self.create_geometry()
-        self.app.proc_container.new_text = ''
-
-    def offset(self, vect):
-        """
-        Offsets geometry on the XY plane in the object by a given vector.
-
-        :param vect: (x, y) offset vector.
-        :type vect: tuple
-        :return: None
-        """
-        log.debug("camlib.Excellon.offset()")
-
-        dx, dy = vect
-
-        def offset_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(offset_geom(g))
-                return new_obj
-            else:
-                try:
-                    return affinity.translate(obj, xoff=dx, yoff=dy)
-                except AttributeError:
-                    return obj
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.drills:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        # Drills
-        for drill in self.drills:
-            drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
-
-            self.el_count += 1
-            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-            if self.old_disp_number < disp_number <= 100:
-                self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                self.old_disp_number = disp_number
-
-        # offset solid_geometry
-        for tool in self.tools:
-            self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry'])
-
-        # Slots
-        for slot in self.slots:
-            slot['stop'] = affinity.translate(slot['stop'], xoff=dx, yoff=dy)
-            slot['start'] = affinity.translate(slot['start'],xoff=dx, yoff=dy)
-
-        # Recreate geometry
-        self.create_geometry()
-        self.app.proc_container.new_text = ''
-
-    def mirror(self, axis, point):
-        """
-
-        :param axis: "X" or "Y" indicates around which axis to mirror.
-        :type axis: str
-        :param point: [x, y] point belonging to the mirror axis.
-        :type point: list
-        :return: None
-        """
-        log.debug("camlib.Excellon.mirror()")
-
-        px, py = point
-        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
-
-        def mirror_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(mirror_geom(g))
-                return new_obj
-            else:
-                try:
-                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
-                except AttributeError:
-                    return obj
-
-        # Modify data
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.drills:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        # Drills
-        for drill in self.drills:
-            drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py))
-
-            self.el_count += 1
-            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-            if self.old_disp_number < disp_number <= 100:
-                self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                self.old_disp_number = disp_number
-
-        # mirror solid_geometry
-        for tool in self.tools:
-            self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
-
-        # Slots
-        for slot in self.slots:
-            slot['stop'] = affinity.scale(slot['stop'], xscale, yscale, origin=(px, py))
-            slot['start'] = affinity.scale(slot['start'], xscale, yscale, origin=(px, py))
-
-        # Recreate geometry
-        self.create_geometry()
-        self.app.proc_container.new_text = ''
-
-    def skew(self, angle_x=None, angle_y=None, point=None):
-        """
-        Shear/Skew the geometries of an object by angles along x and y dimensions.
-        Tool sizes, feedrates an Z-plane dimensions are untouched.
-
-        Parameters
-        ----------
-        xs, ys : float, float
-            The shear angle(s) for the x and y axes respectively. These can be
-            specified in either degrees (default) or radians by setting
-            use_radians=True.
-
-        See shapely manual for more information:
-        http://toblerity.org/shapely/manual.html#affine-transformations
-        """
-        log.debug("camlib.Excellon.skew()")
-
-        if angle_x is None:
-            angle_x = 0.0
-
-        if angle_y is None:
-            angle_y = 0.0
-
-        def skew_geom(obj):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(skew_geom(g))
-                return new_obj
-            else:
-                try:
-                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
-                except AttributeError:
-                    return obj
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.drills:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        if point is None:
-            px, py = 0, 0
-
-            # Drills
-            for drill in self.drills:
-                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
-                                               origin=(px, py))
-
-                self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                if self.old_disp_number < disp_number <= 100:
-                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                    self.old_disp_number = disp_number
-
-            # skew solid_geometry
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
-
-            # Slots
-            for slot in self.slots:
-                slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
-                slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
-        else:
-            px, py = point
-            # Drills
-            for drill in self.drills:
-                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
-                                               origin=(px, py))
-
-                self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                if self.old_disp_number < disp_number <= 100:
-                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                    self.old_disp_number = disp_number
-
-            # skew solid_geometry
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = skew_geom( self.tools[tool]['solid_geometry'])
-
-            # Slots
-            for slot in self.slots:
-                slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
-                slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
-
-        self.create_geometry()
-        self.app.proc_container.new_text = ''
-
-    def rotate(self, angle, point=None):
-        """
-        Rotate the geometry of an object by an angle around the 'point' coordinates
-        :param angle:
-        :param point: tuple of coordinates (x, y)
-        :return:
-        """
-        log.debug("camlib.Excellon.rotate()")
-
-        def rotate_geom(obj, origin=None):
-            if type(obj) is list:
-                new_obj = []
-                for g in obj:
-                    new_obj.append(rotate_geom(g))
-                return new_obj
-            else:
-                if origin:
-                    try:
-                        return affinity.rotate(obj, angle, origin=origin)
-                    except AttributeError:
-                        return obj
-                else:
-                    try:
-                        return affinity.rotate(obj, angle, origin=(px, py))
-                    except AttributeError:
-                        return obj
-
-        # variables to display the percentage of work done
-        self.geo_len = 0
-        try:
-            for g in self.drills:
-                self.geo_len += 1
-        except TypeError:
-            self.geo_len = 1
-        self.old_disp_number = 0
-        self.el_count = 0
-
-        if point is None:
-            # Drills
-            for drill in self.drills:
-                drill['point'] = affinity.rotate(drill['point'], angle, origin='center')
-
-            # rotate solid_geometry
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin='center')
-
-                self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                if self.old_disp_number < disp_number <= 100:
-                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                    self.old_disp_number = disp_number
-
-            # Slots
-            for slot in self.slots:
-                slot['stop'] = affinity.rotate(slot['stop'], angle, origin='center')
-                slot['start'] = affinity.rotate(slot['start'], angle, origin='center')
-        else:
-            px, py = point
-            # Drills
-            for drill in self.drills:
-                drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py))
-
-                self.el_count += 1
-                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
-                if self.old_disp_number < disp_number <= 100:
-                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
-                    self.old_disp_number = disp_number
-
-            # rotate solid_geometry
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'])
-
-            # Slots
-            for slot in self.slots:
-                slot['stop'] = affinity.rotate(slot['stop'], angle, origin=(px, py))
-                slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py))
-
-        self.create_geometry()
-        self.app.proc_container.new_text = ''
-
-
 class AttrDict(dict):
     def __init__(self, *args, **kwargs):
         super(AttrDict, self).__init__(*args, **kwargs)

+ 1 - 0
flatcamEditors/FlatCAMExcEditor.py

@@ -19,6 +19,7 @@ from rtree import index as rtindex
 from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
+from flatcamParsers.ParseExcellon import Excellon
 
 from copy import copy, deepcopy
 

+ 1 - 0
flatcamEditors/FlatCAMGrbEditor.py

@@ -24,6 +24,7 @@ from camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \
     SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
 from FlatCAMObj import FlatCAMGerber
+from flatcamParsers.ParseGerber import Gerber
 from FlatCAMTool import FlatCAMTool
 
 from numpy.linalg import norm as numpy_norm

+ 1433 - 0
flatcamParsers/ParseExcellon.py

@@ -0,0 +1,1433 @@
+from camlib import *
+
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class Excellon(Geometry):
+    """
+    Here it is done all the Excellon parsing.
+
+    *ATTRIBUTES*
+
+    * ``tools`` (dict): The key is the tool name and the value is
+      a dictionary specifying the tool:
+
+    ================  ====================================
+    Key               Value
+    ================  ====================================
+    C                 Diameter of the tool
+    solid_geometry    Geometry list for each tool
+    Others            Not supported (Ignored).
+    ================  ====================================
+
+    * ``drills`` (list): Each is a dictionary:
+
+    ================  ====================================
+    Key               Value
+    ================  ====================================
+    point             (Shapely.Point) Where to drill
+    tool              (str) A key in ``tools``
+    ================  ====================================
+
+    * ``slots`` (list): Each is a dictionary
+
+    ================  ====================================
+    Key               Value
+    ================  ====================================
+    start             (Shapely.Point) Start point of the slot
+    stop              (Shapely.Point) Stop point of the slot
+    tool              (str) A key in ``tools``
+    ================  ====================================
+    """
+
+    defaults = {
+        "zeros": "L",
+        "excellon_format_upper_mm": '3',
+        "excellon_format_lower_mm": '3',
+        "excellon_format_upper_in": '2',
+        "excellon_format_lower_in": '4',
+        "excellon_units": 'INCH',
+        "geo_steps_per_circle": '64'
+    }
+
+    def __init__(self, zeros=None, excellon_format_upper_mm=None, excellon_format_lower_mm=None,
+                 excellon_format_upper_in=None, excellon_format_lower_in=None, excellon_units=None,
+                 geo_steps_per_circle=None):
+        """
+        The constructor takes no parameters.
+
+        :return: Excellon object.
+        :rtype: Excellon
+        """
+
+        if geo_steps_per_circle is None:
+            geo_steps_per_circle = int(Excellon.defaults['geo_steps_per_circle'])
+        self.geo_steps_per_circle = int(geo_steps_per_circle)
+
+        Geometry.__init__(self, geo_steps_per_circle=int(geo_steps_per_circle))
+
+        # dictionary to store tools, see above for description
+        self.tools = {}
+        # list to store the drills, see above for description
+        self.drills = []
+
+        # self.slots (list) to store the slots; each is a dictionary
+        self.slots = []
+
+        self.source_file = ''
+
+        # it serve to flag if a start routing or a stop routing was encountered
+        # if a stop is encounter and this flag is still 0 (so there is no stop for a previous start) issue error
+        self.routing_flag = 1
+
+        self.match_routing_start = None
+        self.match_routing_stop = None
+
+        self.num_tools = []  # List for keeping the tools sorted
+        self.index_per_tool = {}  # Dictionary to store the indexed points for each tool
+
+        # ## IN|MM -> Units are inherited from Geometry
+        # self.units = units
+
+        # Trailing "T" or leading "L" (default)
+        # self.zeros = "T"
+        self.zeros = zeros or self.defaults["zeros"]
+        self.zeros_found = self.zeros
+        self.units_found = self.units
+
+        # this will serve as a default if the Excellon file has no info regarding of tool diameters (this info may be
+        # in another file like for PCB WIzard ECAD software
+        self.toolless_diam = 1.0
+        # signal that the Excellon file has no tool diameter informations and the tools have bogus (random) diameter
+        self.diameterless = False
+
+        # Excellon format
+        self.excellon_format_upper_in = excellon_format_upper_in or self.defaults["excellon_format_upper_in"]
+        self.excellon_format_lower_in = excellon_format_lower_in or self.defaults["excellon_format_lower_in"]
+        self.excellon_format_upper_mm = excellon_format_upper_mm or self.defaults["excellon_format_upper_mm"]
+        self.excellon_format_lower_mm = excellon_format_lower_mm or self.defaults["excellon_format_lower_mm"]
+        self.excellon_units = excellon_units or self.defaults["excellon_units"]
+        # detected Excellon format is stored here:
+        self.excellon_format = None
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from Geometry.
+        self.ser_attrs += ['tools', 'drills', 'zeros', 'excellon_format_upper_mm', 'excellon_format_lower_mm',
+                           'excellon_format_upper_in', 'excellon_format_lower_in', 'excellon_units', 'slots',
+                           'source_file']
+
+        # ### Patterns ####
+        # Regex basics:
+        # ^ - beginning
+        # $ - end
+        # *: 0 or more, +: 1 or more, ?: 0 or 1
+
+        # M48 - Beginning of Part Program Header
+        self.hbegin_re = re.compile(r'^M48$')
+
+        # ;HEADER - Beginning of Allegro Program Header
+        self.allegro_hbegin_re = re.compile(r'\;\s*(HEADER)')
+
+        # M95 or % - End of Part Program Header
+        # NOTE: % has different meaning in the body
+        self.hend_re = re.compile(r'^(?:M95|%)$')
+
+        # FMAT Excellon format
+        # Ignored in the parser
+        # self.fmat_re = re.compile(r'^FMAT,([12])$')
+
+        # Uunits and possible Excellon zeros and possible Excellon format
+        # INCH uses 6 digits
+        # METRIC uses 5/6
+        self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?,?(\d*\.\d+)?.*$')
+
+        # Tool definition/parameters (?= is look-ahead
+        # NOTE: This might be an overkill!
+        # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
+        #                              r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
+        #                              r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
+        #                              r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
+        self.toolset_re = re.compile(r'^T(\d+)(?=.*C,?(\d*\.?\d*))?' +
+                                     r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
+                                     r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
+                                     r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
+
+        self.detect_gcode_re = re.compile(r'^G2([01])$')
+
+        # Tool select
+        # Can have additional data after tool number but
+        # is ignored if present in the header.
+        # Warning: This will match toolset_re too.
+        # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
+        self.toolsel_re = re.compile(r'^T(\d+)')
+
+        # Headerless toolset
+        # self.toolset_hl_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))')
+        self.toolset_hl_re = re.compile(r'^T(\d+)(?:.?C(\d+\.?\d*))?')
+
+        # Comment
+        self.comm_re = re.compile(r'^;(.*)$')
+
+        # Absolute/Incremental G90/G91
+        self.absinc_re = re.compile(r'^G9([01])$')
+
+        # Modes of operation
+        # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
+        self.modes_re = re.compile(r'^G0([012345])')
+
+        # Measuring mode
+        # 1-metric, 2-inch
+        self.meas_re = re.compile(r'^M7([12])$')
+
+        # Coordinates
+        # self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
+        # self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
+        coordsperiod_re_string = r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]'
+        self.coordsperiod_re = re.compile(coordsperiod_re_string)
+
+        coordsnoperiod_re_string = r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]'
+        self.coordsnoperiod_re = re.compile(coordsnoperiod_re_string)
+
+        # Slots parsing
+        slots_re_string = r'^([^G]+)G85(.*)$'
+        self.slots_re = re.compile(slots_re_string)
+
+        # R - Repeat hole (# times, X offset, Y offset)
+        self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$')
+
+        # Various stop/pause commands
+        self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
+
+        # Allegro Excellon format support
+        self.tool_units_re = re.compile(r'(\;\s*Holesize \d+.\s*\=\s*(\d+.\d+).*(MILS|MM))')
+
+        # Altium Excellon format support
+        # it's a comment like this: ";FILE_FORMAT=2:5"
+        self.altium_format = re.compile(r'^;\s*(?:FILE_FORMAT)?(?:Format)?[=|:]\s*(\d+)[:|.](\d+).*$')
+
+        # Parse coordinates
+        self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)')
+
+        # Repeating command
+        self.repeat_re = re.compile(r'R(\d+)')
+
+    def parse_file(self, filename=None, file_obj=None):
+        """
+        Reads the specified file as array of lines as
+        passes it to ``parse_lines()``.
+
+        :param filename: The file to be read and parsed.
+        :type filename: str
+        :return: None
+        """
+        if file_obj:
+            estr = file_obj
+        else:
+            if filename is None:
+                return "fail"
+            efile = open(filename, 'r')
+            estr = efile.readlines()
+            efile.close()
+
+        try:
+            self.parse_lines(estr)
+        except:
+            return "fail"
+
+    def parse_lines(self, elines):
+        """
+        Main Excellon parser.
+
+        :param elines: List of strings, each being a line of Excellon code.
+        :type elines: list
+        :return: None
+        """
+
+        # State variables
+        current_tool = ""
+        in_header = False
+        headerless = False
+        current_x = None
+        current_y = None
+
+        slot_current_x = None
+        slot_current_y = None
+
+        name_tool = 0
+        allegro_warning = False
+        line_units_found = False
+
+        repeating_x = 0
+        repeating_y = 0
+        repeat = 0
+
+        line_units = ''
+
+        #### Parsing starts here ## ##
+        line_num = 0  # Line number
+        eline = ""
+        try:
+            for eline in elines:
+                if self.app.abort_flag:
+                    # graceful abort requested by the user
+                    raise FlatCAMApp.GracefulException
+
+                line_num += 1
+                # log.debug("%3d %s" % (line_num, str(eline)))
+
+                self.source_file += eline
+
+                # Cleanup lines
+                eline = eline.strip(' \r\n')
+
+                # Excellon files and Gcode share some extensions therefore if we detect G20 or G21 it's GCODe
+                # and we need to exit from here
+                if self.detect_gcode_re.search(eline):
+                    log.warning("This is GCODE mark: %s" % eline)
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
+                                         (_('This is GCODE mark'), eline))
+                    return
+
+                # Header Begin (M48) #
+                if self.hbegin_re.search(eline):
+                    in_header = True
+                    headerless = False
+                    log.warning("Found start of the header: %s" % eline)
+                    continue
+
+                # Allegro Header Begin (;HEADER) #
+                if self.allegro_hbegin_re.search(eline):
+                    in_header = True
+                    allegro_warning = True
+                    log.warning("Found ALLEGRO start of the header: %s" % eline)
+                    continue
+
+                # Search for Header End #
+                # Since there might be comments in the header that include header end char (% or M95)
+                # we ignore the lines starting with ';' that contains such header end chars because it is not a
+                # real header end.
+                if self.comm_re.search(eline):
+                    match = self.tool_units_re.search(eline)
+                    if match:
+                        if line_units_found is False:
+                            line_units_found = True
+                            line_units = match.group(3)
+                            self.convert_units({"MILS": "IN", "MM": "MM"}[line_units])
+                            log.warning("Type of Allegro UNITS found inline in comments: %s" % line_units)
+
+                        if match.group(2):
+                            name_tool += 1
+                            if line_units == 'MILS':
+                                spec = {"C": (float(match.group(2)) / 1000)}
+                                self.tools[str(name_tool)] = spec
+                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
+                            else:
+                                spec = {"C": float(match.group(2))}
+                                self.tools[str(name_tool)] = spec
+                                log.debug("  Tool definition: %s %s" % (name_tool, spec))
+                            spec['solid_geometry'] = []
+                            continue
+                    # search for Altium Excellon Format / Sprint Layout who is included as a comment
+                    match = self.altium_format.search(eline)
+                    if match:
+                        self.excellon_format_upper_mm = match.group(1)
+                        self.excellon_format_lower_mm = match.group(2)
+
+                        self.excellon_format_upper_in = match.group(1)
+                        self.excellon_format_lower_in = match.group(2)
+                        log.warning("Altium Excellon format preset found in comments: %s:%s" %
+                                    (match.group(1), match.group(2)))
+                        continue
+                    else:
+                        log.warning("Line ignored, it's a comment: %s" % eline)
+                else:
+                    if self.hend_re.search(eline):
+                        if in_header is False or bool(self.tools) is False:
+                            log.warning("Found end of the header but there is no header: %s" % eline)
+                            log.warning("The only useful data in header are tools, units and format.")
+                            log.warning("Therefore we will create units and format based on defaults.")
+                            headerless = True
+                            try:
+                                self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.excellon_units])
+                            except Exception as e:
+                                log.warning("Units could not be converted: %s" % str(e))
+
+                        in_header = False
+                        # for Allegro type of Excellons we reset name_tool variable so we can reuse it for toolchange
+                        if allegro_warning is True:
+                            name_tool = 0
+                        log.warning("Found end of the header: %s" % eline)
+                        continue
+
+                # ## Alternative units format M71/M72
+                # Supposed to be just in the body (yes, the body)
+                # but some put it in the header (PADS for example).
+                # Will detect anywhere. Occurrence will change the
+                # object's units.
+                match = self.meas_re.match(eline)
+                if match:
+                    # self.units = {"1": "MM", "2": "IN"}[match.group(1)]
+
+                    # Modified for issue #80
+                    self.convert_units({"1": "MM", "2": "IN"}[match.group(1)])
+                    log.debug("  Units: %s" % self.units)
+                    if self.units == 'MM':
+                        log.warning("Excellon format preset is: %s" % self.excellon_format_upper_mm + \
+                                    ':' + str(self.excellon_format_lower_mm))
+                    else:
+                        log.warning("Excellon format preset is: %s" % self.excellon_format_upper_in + \
+                                    ':' + str(self.excellon_format_lower_in))
+                    continue
+
+                # ### Body ####
+                if not in_header:
+
+                    # ## Tool change ###
+                    match = self.toolsel_re.search(eline)
+                    if match:
+                        current_tool = str(int(match.group(1)))
+                        log.debug("Tool change: %s" % current_tool)
+                        if bool(headerless):
+                            match = self.toolset_hl_re.search(eline)
+                            if match:
+                                name = str(int(match.group(1)))
+                                try:
+                                    diam = float(match.group(2))
+                                except:
+                                    # it's possible that tool definition has only tool number and no diameter info
+                                    # (those could be in another file like PCB Wizard do)
+                                    # then match.group(2) = None and float(None) will create the exception
+                                    # the bellow construction is so each tool will have a slightly different diameter
+                                    # starting with a default value, to allow Excellon editing after that
+                                    self.diameterless = True
+                                    self.app.inform.emit('[WARNING] %s%s %s' %
+                                                         (_("No tool diameter info's. See shell.\n"
+                                                            "A tool change event: T"),
+                                                          str(current_tool),
+                                                          _("was found but the Excellon file "
+                                                            "have no informations regarding the tool "
+                                                            "diameters therefore the application will try to load it "
+                                                            "by using some 'fake' diameters.\n"
+                                                            "The user needs to edit the resulting Excellon object and "
+                                                            "change the diameters to reflect the real diameters.")
+                                                          )
+                                                         )
+
+                                    if self.excellon_units == 'MM':
+                                        diam = self.toolless_diam + (int(current_tool) - 1) / 100
+                                    else:
+                                        diam = (self.toolless_diam + (int(current_tool) - 1) / 100) / 25.4
+
+                                spec = {"C": diam, 'solid_geometry': []}
+                                self.tools[name] = spec
+                                log.debug("Tool definition out of header: %s %s" % (name, spec))
+
+                        continue
+
+                    # ## Allegro Type Tool change ###
+                    if allegro_warning is True:
+                        match = self.absinc_re.search(eline)
+                        match1 = self.stop_re.search(eline)
+                        if match or match1:
+                            name_tool += 1
+                            current_tool = str(name_tool)
+                            log.debug("Tool change for Allegro type of Excellon: %s" % current_tool)
+                            continue
+
+                    # ## Slots parsing for drilled slots (contain G85)
+                    # a Excellon drilled slot line may look like this:
+                    # X01125Y0022244G85Y0027756
+                    match = self.slots_re.search(eline)
+                    if match:
+                        # signal that there are milling slots operations
+                        self.defaults['excellon_drills'] = False
+
+                        # the slot start coordinates group is to the left of G85 command (group(1) )
+                        # the slot stop coordinates group is to the right of G85 command (group(2) )
+                        start_coords_match = match.group(1)
+                        stop_coords_match = match.group(2)
+
+                        # Slot coordinates without period # ##
+                        # get the coordinates for slot start and for slot stop into variables
+                        start_coords_noperiod = self.coordsnoperiod_re.search(start_coords_match)
+                        stop_coords_noperiod = self.coordsnoperiod_re.search(stop_coords_match)
+                        if start_coords_noperiod:
+                            try:
+                                slot_start_x = self.parse_number(start_coords_noperiod.group(1))
+                                slot_current_x = slot_start_x
+                            except TypeError:
+                                slot_start_x = slot_current_x
+                            except:
+                                return
+
+                            try:
+                                slot_start_y = self.parse_number(start_coords_noperiod.group(2))
+                                slot_current_y = slot_start_y
+                            except TypeError:
+                                slot_start_y = slot_current_y
+                            except:
+                                return
+
+                            try:
+                                slot_stop_x = self.parse_number(stop_coords_noperiod.group(1))
+                                slot_current_x = slot_stop_x
+                            except TypeError:
+                                slot_stop_x = slot_current_x
+                            except:
+                                return
+
+                            try:
+                                slot_stop_y = self.parse_number(stop_coords_noperiod.group(2))
+                                slot_current_y = slot_stop_y
+                            except TypeError:
+                                slot_stop_y = slot_current_y
+                            except:
+                                return
+
+                            if (slot_start_x is None or slot_start_y is None or
+                                    slot_stop_x is None or slot_stop_y is None):
+                                log.error("Slots are missing some or all coordinates.")
+                                continue
+
+                            # we have a slot
+                            log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
+                                                                                slot_start_y, slot_stop_x,
+                                                                                slot_stop_y]))
+
+                            # store current tool diameter as slot diameter
+                            slot_dia = 0.05
+                            try:
+                                slot_dia = float(self.tools[current_tool]['C'])
+                            except Exception as e:
+                                pass
+                            log.debug(
+                                'Milling/Drilling slot with tool %s, diam=%f' % (
+                                    current_tool,
+                                    slot_dia
+                                )
+                            )
+
+                            self.slots.append(
+                                {
+                                    'start': Point(slot_start_x, slot_start_y),
+                                    'stop': Point(slot_stop_x, slot_stop_y),
+                                    'tool': current_tool
+                                }
+                            )
+                            continue
+
+                        # Slot coordinates with period: Use literally. ###
+                        # get the coordinates for slot start and for slot stop into variables
+                        start_coords_period = self.coordsperiod_re.search(start_coords_match)
+                        stop_coords_period = self.coordsperiod_re.search(stop_coords_match)
+                        if start_coords_period:
+
+                            try:
+                                slot_start_x = float(start_coords_period.group(1))
+                                slot_current_x = slot_start_x
+                            except TypeError:
+                                slot_start_x = slot_current_x
+                            except:
+                                return
+
+                            try:
+                                slot_start_y = float(start_coords_period.group(2))
+                                slot_current_y = slot_start_y
+                            except TypeError:
+                                slot_start_y = slot_current_y
+                            except:
+                                return
+
+                            try:
+                                slot_stop_x = float(stop_coords_period.group(1))
+                                slot_current_x = slot_stop_x
+                            except TypeError:
+                                slot_stop_x = slot_current_x
+                            except:
+                                return
+
+                            try:
+                                slot_stop_y = float(stop_coords_period.group(2))
+                                slot_current_y = slot_stop_y
+                            except TypeError:
+                                slot_stop_y = slot_current_y
+                            except:
+                                return
+
+                            if (slot_start_x is None or slot_start_y is None or
+                                    slot_stop_x is None or slot_stop_y is None):
+                                log.error("Slots are missing some or all coordinates.")
+                                continue
+
+                            # we have a slot
+                            log.debug('Parsed a slot with coordinates: ' + str([slot_start_x,
+                                                                                slot_start_y, slot_stop_x,
+                                                                                slot_stop_y]))
+
+                            # store current tool diameter as slot diameter
+                            slot_dia = 0.05
+                            try:
+                                slot_dia = float(self.tools[current_tool]['C'])
+                            except Exception as e:
+                                pass
+                            log.debug(
+                                'Milling/Drilling slot with tool %s, diam=%f' % (
+                                    current_tool,
+                                    slot_dia
+                                )
+                            )
+
+                            self.slots.append(
+                                {
+                                    'start': Point(slot_start_x, slot_start_y),
+                                    'stop': Point(slot_stop_x, slot_stop_y),
+                                    'tool': current_tool
+                                }
+                            )
+                        continue
+
+                    # ## Coordinates without period # ##
+                    match = self.coordsnoperiod_re.search(eline)
+                    if match:
+                        matchr = self.repeat_re.search(eline)
+                        if matchr:
+                            repeat = int(matchr.group(1))
+
+                        try:
+                            x = self.parse_number(match.group(1))
+                            repeating_x = current_x
+                            current_x = x
+                        except TypeError:
+                            x = current_x
+                            repeating_x = 0
+                        except:
+                            return
+
+                        try:
+                            y = self.parse_number(match.group(2))
+                            repeating_y = current_y
+                            current_y = y
+                        except TypeError:
+                            y = current_y
+                            repeating_y = 0
+                        except:
+                            return
+
+                        if x is None or y is None:
+                            log.error("Missing coordinates")
+                            continue
+
+                        # ## Excellon Routing parse
+                        if len(re.findall("G00", eline)) > 0:
+                            self.match_routing_start = 'G00'
+
+                            # signal that there are milling slots operations
+                            self.defaults['excellon_drills'] = False
+
+                            self.routing_flag = 0
+                            slot_start_x = x
+                            slot_start_y = y
+                            continue
+
+                        if self.routing_flag == 0:
+                            if len(re.findall("G01", eline)) > 0:
+                                self.match_routing_stop = 'G01'
+
+                                # signal that there are milling slots operations
+                                self.defaults['excellon_drills'] = False
+
+                                self.routing_flag = 1
+                                slot_stop_x = x
+                                slot_stop_y = y
+                                self.slots.append(
+                                    {
+                                        'start': Point(slot_start_x, slot_start_y),
+                                        'stop': Point(slot_stop_x, slot_stop_y),
+                                        'tool': current_tool
+                                    }
+                                )
+                                continue
+
+                        if self.match_routing_start is None and self.match_routing_stop is None:
+                            if repeat == 0:
+                                # signal that there are drill operations
+                                self.defaults['excellon_drills'] = True
+                                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
+                            else:
+                                coordx = x
+                                coordy = y
+                                while repeat > 0:
+                                    if repeating_x:
+                                        coordx = (repeat * x) + repeating_x
+                                    if repeating_y:
+                                        coordy = (repeat * y) + repeating_y
+                                    self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
+                                    repeat -= 1
+                            repeating_x = repeating_y = 0
+                            # log.debug("{:15} {:8} {:8}".format(eline, x, y))
+                            continue
+
+                    # ## Coordinates with period: Use literally. # ##
+                    match = self.coordsperiod_re.search(eline)
+                    if match:
+                        matchr = self.repeat_re.search(eline)
+                        if matchr:
+                            repeat = int(matchr.group(1))
+
+                    if match:
+                        # signal that there are drill operations
+                        self.defaults['excellon_drills'] = True
+                        try:
+                            x = float(match.group(1))
+                            repeating_x = current_x
+                            current_x = x
+                        except TypeError:
+                            x = current_x
+                            repeating_x = 0
+
+                        try:
+                            y = float(match.group(2))
+                            repeating_y = current_y
+                            current_y = y
+                        except TypeError:
+                            y = current_y
+                            repeating_y = 0
+
+                        if x is None or y is None:
+                            log.error("Missing coordinates")
+                            continue
+
+                        # ## Excellon Routing parse
+                        if len(re.findall("G00", eline)) > 0:
+                            self.match_routing_start = 'G00'
+
+                            # signal that there are milling slots operations
+                            self.defaults['excellon_drills'] = False
+
+                            self.routing_flag = 0
+                            slot_start_x = x
+                            slot_start_y = y
+                            continue
+
+                        if self.routing_flag == 0:
+                            if len(re.findall("G01", eline)) > 0:
+                                self.match_routing_stop = 'G01'
+
+                                # signal that there are milling slots operations
+                                self.defaults['excellon_drills'] = False
+
+                                self.routing_flag = 1
+                                slot_stop_x = x
+                                slot_stop_y = y
+                                self.slots.append(
+                                    {
+                                        'start': Point(slot_start_x, slot_start_y),
+                                        'stop': Point(slot_stop_x, slot_stop_y),
+                                        'tool': current_tool
+                                    }
+                                )
+                                continue
+
+                        if self.match_routing_start is None and self.match_routing_stop is None:
+                            # signal that there are drill operations
+                            if repeat == 0:
+                                # signal that there are drill operations
+                                self.defaults['excellon_drills'] = True
+                                self.drills.append({'point': Point((x, y)), 'tool': current_tool})
+                            else:
+                                coordx = x
+                                coordy = y
+                                while repeat > 0:
+                                    if repeating_x:
+                                        coordx = (repeat * x) + repeating_x
+                                    if repeating_y:
+                                        coordy = (repeat * y) + repeating_y
+                                    self.drills.append({'point': Point((coordx, coordy)), 'tool': current_tool})
+                                    repeat -= 1
+                            repeating_x = repeating_y = 0
+                            # log.debug("{:15} {:8} {:8}".format(eline, x, y))
+                            continue
+
+                # ### Header ####
+                if in_header:
+
+                    # ## Tool definitions # ##
+                    match = self.toolset_re.search(eline)
+                    if match:
+                        name = str(int(match.group(1)))
+                        spec = {"C": float(match.group(2)), 'solid_geometry': []}
+                        self.tools[name] = spec
+                        log.debug("  Tool definition: %s %s" % (name, spec))
+                        continue
+
+                    # ## Units and number format # ##
+                    match = self.units_re.match(eline)
+                    if match:
+                        self.units_found = match.group(1)
+                        self.zeros = match.group(2)  # "T" or "L". Might be empty
+                        self.excellon_format = match.group(3)
+                        if self.excellon_format:
+                            upper = len(self.excellon_format.partition('.')[0])
+                            lower = len(self.excellon_format.partition('.')[2])
+                            if self.units == 'MM':
+                                self.excellon_format_upper_mm = upper
+                                self.excellon_format_lower_mm = lower
+                            else:
+                                self.excellon_format_upper_in = upper
+                                self.excellon_format_lower_in = lower
+
+                        # Modified for issue #80
+                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
+                        # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
+                        log.warning("Units: %s" % self.units)
+                        if self.units == 'MM':
+                            log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
+                                        ':' + str(self.excellon_format_lower_mm))
+                        else:
+                            log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
+                                        ':' + str(self.excellon_format_lower_in))
+                        log.warning("Type of zeros found inline: %s" % self.zeros)
+                        continue
+
+                    # Search for units type again it might be alone on the line
+                    if "INCH" in eline:
+                        line_units = "INCH"
+                        # Modified for issue #80
+                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
+                        log.warning("Type of UNITS found inline: %s" % line_units)
+                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
+                                    ':' + str(self.excellon_format_lower_in))
+                        # TODO: not working
+                        # FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
+                        continue
+                    elif "METRIC" in eline:
+                        line_units = "METRIC"
+                        # Modified for issue #80
+                        self.convert_units({"INCH": "IN", "METRIC": "MM"}[line_units])
+                        log.warning("Type of UNITS found inline: %s" % line_units)
+                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
+                                    ':' + str(self.excellon_format_lower_mm))
+                        # TODO: not working
+                        # FlatCAMApp.App.inform.emit("Detected INLINE: %s" % str(eline))
+                        continue
+
+                    # Search for zeros type again because it might be alone on the line
+                    match = re.search(r'[LT]Z', eline)
+                    if match:
+                        self.zeros = match.group()
+                        log.warning("Type of zeros found: %s" % self.zeros)
+                        continue
+
+                # ## Units and number format outside header# ##
+                match = self.units_re.match(eline)
+                if match:
+                    self.units_found = match.group(1)
+                    self.zeros = match.group(2)  # "T" or "L". Might be empty
+                    self.excellon_format = match.group(3)
+                    if self.excellon_format:
+                        upper = len(self.excellon_format.partition('.')[0])
+                        lower = len(self.excellon_format.partition('.')[2])
+                        if self.units == 'MM':
+                            self.excellon_format_upper_mm = upper
+                            self.excellon_format_lower_mm = lower
+                        else:
+                            self.excellon_format_upper_in = upper
+                            self.excellon_format_lower_in = lower
+
+                    # Modified for issue #80
+                    self.convert_units({"INCH": "IN", "METRIC": "MM"}[self.units_found])
+                    # log.warning("  Units/Format: %s %s" % (self.units, self.zeros))
+                    log.warning("Units: %s" % self.units)
+                    if self.units == 'MM':
+                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_mm) +
+                                    ':' + str(self.excellon_format_lower_mm))
+                    else:
+                        log.warning("Excellon format preset is: %s" % str(self.excellon_format_upper_in) +
+                                    ':' + str(self.excellon_format_lower_in))
+                    log.warning("Type of zeros found outside header, inline: %s" % self.zeros)
+
+                    log.warning("UNITS found outside header")
+                    continue
+
+                log.warning("Line ignored: %s" % eline)
+
+            # make sure that since we are in headerless mode, we convert the tools only after the file parsing
+            # is finished since the tools definitions are spread in the Excellon body. We use as units the value
+            # from self.defaults['excellon_units']
+            log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
+        except Exception as e:
+            log.error("Excellon PARSING FAILED. Line %d: %s" % (line_num, eline))
+            msg = '[ERROR_NOTCL] %s' % \
+                  _("An internal error has ocurred. See shell.\n")
+            msg += _('{e_code} Excellon Parser error.\nParsing Failed. Line {l_nr}: {line}\n').format(
+                e_code='[ERROR]',
+                l_nr=line_num,
+                line=eline)
+            msg += traceback.format_exc()
+            self.app.inform.emit(msg)
+
+            return "fail"
+
+    def parse_number(self, number_str):
+        """
+        Parses coordinate numbers without period.
+
+        :param number_str: String representing the numerical value.
+        :type number_str: str
+        :return: Floating point representation of the number
+        :rtype: float
+        """
+
+        match = self.leadingzeros_re.search(number_str)
+        nr_length = len(match.group(1)) + len(match.group(2))
+        try:
+            if self.zeros == "L" or self.zeros == "LZ":  # Leading
+                # With leading zeros, when you type in a coordinate,
+                # the leading zeros must always be included.  Trailing zeros
+                # are unneeded and may be left off. The CNC-7 will automatically add them.
+                # r'^[-\+]?(0*)(\d*)'
+                # 6 digits are divided by 10^4
+                # If less than size digits, they are automatically added,
+                # 5 digits then are divided by 10^3 and so on.
+
+                if self.units.lower() == "in":
+                    result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_in)))
+                else:
+                    result = float(number_str) / (10 ** (float(nr_length) - float(self.excellon_format_upper_mm)))
+                return result
+            else:  # Trailing
+                # You must show all zeros to the right of the number and can omit
+                # all zeros to the left of the number. The CNC-7 will count the number
+                # of digits you typed and automatically fill in the missing zeros.
+                # ## flatCAM expects 6digits
+                # flatCAM expects the number of digits entered into the defaults
+
+                if self.units.lower() == "in":  # Inches is 00.0000
+                    result = float(number_str) / (10 ** (float(self.excellon_format_lower_in)))
+                else:  # Metric is 000.000
+                    result = float(number_str) / (10 ** (float(self.excellon_format_lower_mm)))
+                return result
+        except Exception as e:
+            log.error("Aborted. Operation could not be completed due of %s" % str(e))
+            return
+
+    def create_geometry(self):
+        """
+        Creates circles of the tool diameter at every point
+        specified in ``self.drills``. Also creates geometries (polygons)
+        for the slots as specified in ``self.slots``
+        All the resulting geometry is stored into self.solid_geometry list.
+        The list self.solid_geometry has 2 elements: first is a dict with the drills geometry,
+        and second element is another similar dict that contain the slots geometry.
+
+        Each dict has as keys the tool diameters and as values lists with Shapely objects, the geometries
+        ================  ====================================
+        Key               Value
+        ================  ====================================
+        tool_diameter     list of (Shapely.Point) Where to drill
+        ================  ====================================
+
+        :return: None
+        """
+        self.solid_geometry = []
+        try:
+            # clear the solid_geometry in self.tools
+            for tool in self.tools:
+                try:
+                    self.tools[tool]['solid_geometry'][:] = []
+                except KeyError:
+                    self.tools[tool]['solid_geometry'] = []
+
+            for drill in self.drills:
+                # poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
+                if drill['tool'] is '':
+                    self.app.inform.emit('[WARNING] %s' %
+                                         _("Excellon.create_geometry() -> a drill location was skipped "
+                                           "due of not having a tool associated.\n"
+                                           "Check the resulting GCode."))
+                    log.debug("Excellon.create_geometry() -> a drill location was skipped "
+                              "due of not having a tool associated")
+                    continue
+                tooldia = self.tools[drill['tool']]['C']
+                poly = drill['point'].buffer(tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
+                self.solid_geometry.append(poly)
+                self.tools[drill['tool']]['solid_geometry'].append(poly)
+
+            for slot in self.slots:
+                slot_tooldia = self.tools[slot['tool']]['C']
+                start = slot['start']
+                stop = slot['stop']
+
+                lines_string = LineString([start, stop])
+                poly = lines_string.buffer(slot_tooldia / 2.0, int(int(self.geo_steps_per_circle) / 4))
+                self.solid_geometry.append(poly)
+                self.tools[slot['tool']]['solid_geometry'].append(poly)
+
+        except Exception as e:
+            log.debug("Excellon geometry creation failed due of ERROR: %s" % str(e))
+            return "fail"
+
+        # drill_geometry = {}
+        # slot_geometry = {}
+        #
+        # def insertIntoDataStruct(dia, drill_geo, aDict):
+        #     if not dia in aDict:
+        #         aDict[dia] = [drill_geo]
+        #     else:
+        #         aDict[dia].append(drill_geo)
+        #
+        # for tool in self.tools:
+        #     tooldia = self.tools[tool]['C']
+        #     for drill in self.drills:
+        #         if drill['tool'] == tool:
+        #             poly = drill['point'].buffer(tooldia / 2.0)
+        #             insertIntoDataStruct(tooldia, poly, drill_geometry)
+        #
+        # for tool in self.tools:
+        #     slot_tooldia = self.tools[tool]['C']
+        #     for slot in self.slots:
+        #         if slot['tool'] == tool:
+        #             start = slot['start']
+        #             stop = slot['stop']
+        #             lines_string = LineString([start, stop])
+        #             poly = lines_string.buffer(slot_tooldia/2.0, self.geo_steps_per_circle)
+        #             insertIntoDataStruct(slot_tooldia, poly, drill_geometry)
+        #
+        # self.solid_geometry = [drill_geometry, slot_geometry]
+
+    def bounds(self):
+        """
+        Returns coordinates of rectangular bounds
+        of Excellon geometry: (xmin, ymin, xmax, ymax).
+        """
+        # fixed issue of getting bounds only for one level lists of objects
+        # now it can get bounds for nested lists of objects
+
+        log.debug("camlib.Excellon.bounds()")
+        if self.solid_geometry is None:
+            log.debug("solid_geometry is None")
+            return 0, 0, 0, 0
+
+        def bounds_rec(obj):
+            if type(obj) is list:
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in obj:
+                    if type(k) is dict:
+                        for key in k:
+                            minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
+                            minx = min(minx, minx_)
+                            miny = min(miny, miny_)
+                            maxx = max(maxx, maxx_)
+                            maxy = max(maxy, maxy_)
+                    else:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                        minx = min(minx, minx_)
+                        miny = min(miny, miny_)
+                        maxx = max(maxx, maxx_)
+                        maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                return obj.bounds
+
+        minx_list = []
+        miny_list = []
+        maxx_list = []
+        maxy_list = []
+
+        for tool in self.tools:
+            minx, miny, maxx, maxy = bounds_rec(self.tools[tool]['solid_geometry'])
+            minx_list.append(minx)
+            miny_list.append(miny)
+            maxx_list.append(maxx)
+            maxy_list.append(maxy)
+
+        return (min(minx_list), min(miny_list), max(maxx_list), max(maxy_list))
+
+    def convert_units(self, units):
+        """
+        This function first convert to the the units found in the Excellon file but it converts tools that
+        are not there yet so it has no effect other than it signal that the units are the ones in the file.
+
+        On object creation, in new_object(), true conversion is done because this is done at the end of the
+        Excellon file parsing, the tools are inside and self.tools is really converted from the units found
+        inside the file to the FlatCAM units.
+
+        Kind of convolute way to make the conversion and it is based on the assumption that the Excellon file
+        will have detected the units before the tools are parsed and stored in self.tools
+        :param units:
+        :type str: IN or MM
+        :return:
+        """
+        log.debug("camlib.Excellon.convert_units()")
+
+        factor = Geometry.convert_units(self, units)
+
+        # Tools
+        for tname in self.tools:
+            self.tools[tname]["C"] *= factor
+
+        self.create_geometry()
+
+        return factor
+
+    def scale(self, xfactor, yfactor=None, point=None):
+        """
+        Scales geometry on the XY plane in the object by a given factor.
+        Tool sizes, feedrates an Z-plane dimensions are untouched.
+
+        :param factor: Number by which to scale the object.
+        :type factor: float
+        :return: None
+        :rtype: NOne
+        """
+        log.debug("camlib.Excellon.scale()")
+
+        if yfactor is None:
+            yfactor = xfactor
+
+        if point is None:
+            px = 0
+            py = 0
+        else:
+            px, py = point
+
+        def scale_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(scale_geom(g))
+                return new_obj
+            else:
+                try:
+                    return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.scale(drill['point'], xfactor, yfactor, origin=(px, py))
+
+            self.el_count += 1
+            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+            if self.old_disp_number < disp_number <= 100:
+                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                self.old_disp_number = disp_number
+
+        # scale solid_geometry
+        for tool in self.tools:
+            self.tools[tool]['solid_geometry'] = scale_geom(self.tools[tool]['solid_geometry'])
+
+        # Slots
+        for slot in self.slots:
+            slot['stop'] = affinity.scale(slot['stop'], xfactor, yfactor, origin=(px, py))
+            slot['start'] = affinity.scale(slot['start'], xfactor, yfactor, origin=(px, py))
+
+        self.create_geometry()
+        self.app.proc_container.new_text = ''
+
+    def offset(self, vect):
+        """
+        Offsets geometry on the XY plane in the object by a given vector.
+
+        :param vect: (x, y) offset vector.
+        :type vect: tuple
+        :return: None
+        """
+        log.debug("camlib.Excellon.offset()")
+
+        dx, dy = vect
+
+        def offset_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(offset_geom(g))
+                return new_obj
+            else:
+                try:
+                    return affinity.translate(obj, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return obj
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
+
+            self.el_count += 1
+            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+            if self.old_disp_number < disp_number <= 100:
+                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                self.old_disp_number = disp_number
+
+        # offset solid_geometry
+        for tool in self.tools:
+            self.tools[tool]['solid_geometry'] = offset_geom(self.tools[tool]['solid_geometry'])
+
+        # Slots
+        for slot in self.slots:
+            slot['stop'] = affinity.translate(slot['stop'], xoff=dx, yoff=dy)
+            slot['start'] = affinity.translate(slot['start'], xoff=dx, yoff=dy)
+
+        # Recreate geometry
+        self.create_geometry()
+        self.app.proc_container.new_text = ''
+
+    def mirror(self, axis, point):
+        """
+
+        :param axis: "X" or "Y" indicates around which axis to mirror.
+        :type axis: str
+        :param point: [x, y] point belonging to the mirror axis.
+        :type point: list
+        :return: None
+        """
+        log.debug("camlib.Excellon.mirror()")
+
+        px, py = point
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        def mirror_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(mirror_geom(g))
+                return new_obj
+            else:
+                try:
+                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        # Modify data
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        # Drills
+        for drill in self.drills:
+            drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py))
+
+            self.el_count += 1
+            disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+            if self.old_disp_number < disp_number <= 100:
+                self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                self.old_disp_number = disp_number
+
+        # mirror solid_geometry
+        for tool in self.tools:
+            self.tools[tool]['solid_geometry'] = mirror_geom(self.tools[tool]['solid_geometry'])
+
+        # Slots
+        for slot in self.slots:
+            slot['stop'] = affinity.scale(slot['stop'], xscale, yscale, origin=(px, py))
+            slot['start'] = affinity.scale(slot['start'], xscale, yscale, origin=(px, py))
+
+        # Recreate geometry
+        self.create_geometry()
+        self.app.proc_container.new_text = ''
+
+    def skew(self, angle_x=None, angle_y=None, point=None):
+        """
+        Shear/Skew the geometries of an object by angles along x and y dimensions.
+        Tool sizes, feedrates an Z-plane dimensions are untouched.
+
+        Parameters
+        ----------
+        xs, ys : float, float
+            The shear angle(s) for the x and y axes respectively. These can be
+            specified in either degrees (default) or radians by setting
+            use_radians=True.
+
+        See shapely manual for more information:
+        http://toblerity.org/shapely/manual.html#affine-transformations
+        """
+        log.debug("camlib.Excellon.skew()")
+
+        if angle_x is None:
+            angle_x = 0.0
+
+        if angle_y is None:
+            angle_y = 0.0
+
+        def skew_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(skew_geom(g))
+                return new_obj
+            else:
+                try:
+                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        if point is None:
+            px, py = 0, 0
+
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
+                                               origin=(px, py))
+
+                self.el_count += 1
+                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                if self.old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                    self.old_disp_number = disp_number
+
+            # skew solid_geometry
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
+
+            # Slots
+            for slot in self.slots:
+                slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
+                slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
+        else:
+            px, py = point
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.skew(drill['point'], angle_x, angle_y,
+                                               origin=(px, py))
+
+                self.el_count += 1
+                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                if self.old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                    self.old_disp_number = disp_number
+
+            # skew solid_geometry
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = skew_geom(self.tools[tool]['solid_geometry'])
+
+            # Slots
+            for slot in self.slots:
+                slot['stop'] = affinity.skew(slot['stop'], angle_x, angle_y, origin=(px, py))
+                slot['start'] = affinity.skew(slot['start'], angle_x, angle_y, origin=(px, py))
+
+        self.create_geometry()
+        self.app.proc_container.new_text = ''
+
+    def rotate(self, angle, point=None):
+        """
+        Rotate the geometry of an object by an angle around the 'point' coordinates
+        :param angle:
+        :param point: tuple of coordinates (x, y)
+        :return:
+        """
+        log.debug("camlib.Excellon.rotate()")
+
+        def rotate_geom(obj, origin=None):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(rotate_geom(g))
+                return new_obj
+            else:
+                if origin:
+                    try:
+                        return affinity.rotate(obj, angle, origin=origin)
+                    except AttributeError:
+                        return obj
+                else:
+                    try:
+                        return affinity.rotate(obj, angle, origin=(px, py))
+                    except AttributeError:
+                        return obj
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.drills:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        if point is None:
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.rotate(drill['point'], angle, origin='center')
+
+            # rotate solid_geometry
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'], origin='center')
+
+                self.el_count += 1
+                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                if self.old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                    self.old_disp_number = disp_number
+
+            # Slots
+            for slot in self.slots:
+                slot['stop'] = affinity.rotate(slot['stop'], angle, origin='center')
+                slot['start'] = affinity.rotate(slot['start'], angle, origin='center')
+        else:
+            px, py = point
+            # Drills
+            for drill in self.drills:
+                drill['point'] = affinity.rotate(drill['point'], angle, origin=(px, py))
+
+                self.el_count += 1
+                disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                if self.old_disp_number < disp_number <= 100:
+                    self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                    self.old_disp_number = disp_number
+
+            # rotate solid_geometry
+            for tool in self.tools:
+                self.tools[tool]['solid_geometry'] = rotate_geom(self.tools[tool]['solid_geometry'])
+
+            # Slots
+            for slot in self.slots:
+                slot['stop'] = affinity.rotate(slot['stop'], angle, origin=(px, py))
+                slot['start'] = affinity.rotate(slot['start'], angle, origin=(px, py))
+
+        self.create_geometry()
+        self.app.proc_container.new_text = ''

+ 1976 - 0
flatcamParsers/ParseGerber.py

@@ -0,0 +1,1976 @@
+
+from camlib import *
+
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class Gerber(Geometry):
+    """
+    Here it is done all the Gerber parsing.
+
+    **ATTRIBUTES**
+
+    * ``apertures`` (dict): The keys are names/identifiers of each aperture.
+      The values are dictionaries key/value pairs which describe the aperture. The
+      type key is always present and the rest depend on the key:
+
+    +-----------+-----------------------------------+
+    | Key       | Value                             |
+    +===========+===================================+
+    | type      | (str) "C", "R", "O", "P", or "AP" |
+    +-----------+-----------------------------------+
+    | others    | Depend on ``type``                |
+    +-----------+-----------------------------------+
+    | solid_geometry      | (list)                  |
+    +-----------+-----------------------------------+
+    * ``aperture_macros`` (dictionary): Are predefined geometrical structures
+      that can be instantiated with different parameters in an aperture
+      definition. See ``apertures`` above. The key is the name of the macro,
+      and the macro itself, the value, is a ``Aperture_Macro`` object.
+
+    * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
+      from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
+
+    * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
+      *buffering* (or thickening) the ``paths`` with the aperture. These are
+      generated from ``paths`` in ``buffer_paths()``.
+
+    **USAGE**::
+
+        g = Gerber()
+        g.parse_file(filename)
+        g.create_geometry()
+        do_something(s.solid_geometry)
+
+    """
+
+    # defaults = {
+    #     "steps_per_circle": 128,
+    #     "use_buffer_for_union": True
+    # }
+
+    def __init__(self, steps_per_circle=None):
+        """
+        The constructor takes no parameters. Use ``gerber.parse_files()``
+        or ``gerber.parse_lines()`` to populate the object from Gerber source.
+
+        :return: Gerber object
+        :rtype: Gerber
+        """
+
+        # How to approximate a circle with lines.
+        self.steps_per_circle = int(self.app.defaults["gerber_circle_steps"])
+
+        # Initialize parent
+        Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["gerber_circle_steps"]))
+
+        # Number format
+        self.int_digits = 3
+        """Number of integer digits in Gerber numbers. Used during parsing."""
+
+        self.frac_digits = 4
+        """Number of fraction digits in Gerber numbers. Used during parsing."""
+
+        self.gerber_zeros = self.app.defaults['gerber_def_zeros']
+        """Zeros in Gerber numbers. If 'L' then remove leading zeros, if 'T' remove trailing zeros. Used during parsing.
+        """
+
+        # ## Gerber elements # ##
+        '''
+        apertures = {
+            'id':{
+                'type':string, 
+                'size':float, 
+                'width':float,
+                'height':float,
+                'geometry': [],
+            }
+        }
+        apertures['geometry'] list elements are dicts
+        dict = {
+            'solid': [],
+            'follow': [],
+            'clear': []
+        }
+        '''
+
+        # store the file units here:
+        self.gerber_units = self.app.defaults['gerber_def_units']
+
+        # aperture storage
+        self.apertures = {}
+
+        # Aperture Macros
+        self.aperture_macros = {}
+
+        # will store the Gerber geometry's as solids
+        self.solid_geometry = Polygon()
+
+        # will store the Gerber geometry's as paths
+        self.follow_geometry = []
+
+        # made True when the LPC command is encountered in Gerber parsing
+        # it allows adding data into the clear_geometry key of the self.apertures[aperture] dict
+        self.is_lpc = False
+
+        self.source_file = ''
+
+        # Attributes to be included in serialization
+        # Always append to it because it carries contents
+        # from Geometry.
+        self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
+                           'aperture_macros', 'solid_geometry', 'source_file']
+
+        # ### Parser patterns ## ##
+        # FS - Format Specification
+        # The format of X and Y must be the same!
+        # L-omit leading zeros, T-omit trailing zeros, D-no zero supression
+        # A-absolute notation, I-incremental notation
+        self.fmt_re = re.compile(r'%?FS([LTD])?([AI])X(\d)(\d)Y\d\d\*%?$')
+        self.fmt_re_alt = re.compile(r'%FS([LTD])?([AI])X(\d)(\d)Y\d\d\*MO(IN|MM)\*%$')
+        self.fmt_re_orcad = re.compile(r'(G\d+)*\**%FS([LTD])?([AI]).*X(\d)(\d)Y\d\d\*%$')
+
+        # Mode (IN/MM)
+        self.mode_re = re.compile(r'^%?MO(IN|MM)\*%?$')
+
+        # Comment G04|G4
+        self.comm_re = re.compile(r'^G0?4(.*)$')
+
+        # AD - Aperture definition
+        # Aperture Macro names: Name = [a-zA-Z_.$]{[a-zA-Z_.0-9]+}
+        # NOTE: Adding "-" to support output from Upverter.
+        self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.\-]*)(?:,(.*))?\*%$')
+
+        # AM - Aperture Macro
+        # Beginning of macro (Ends with *%):
+        # self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
+
+        # Tool change
+        # May begin with G54 but that is deprecated
+        self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
+
+        # G01... - Linear interpolation plus flashes with coordinates
+        # Operation code (D0x) missing is deprecated... oh well I will support it.
+        self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
+
+        # Operation code alone, usually just D03 (Flash)
+        self.opcode_re = re.compile(r'^D0?([123])\*$')
+
+        # G02/3... - Circular interpolation with coordinates
+        # 2-clockwise, 3-counterclockwise
+        # Operation code (D0x) missing is deprecated... oh well I will support it.
+        # Optional start with G02 or G03, optional end with D01 or D02 with
+        # optional coordinates but at least one in any order.
+        self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X([\+-]?\d+))?(?=.*Y([\+-]?\d+))' +
+                                  '?(?=.*I([\+-]?\d+))?(?=.*J([\+-]?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
+
+        # G01/2/3 Occurring without coordinates
+        self.interp_re = re.compile(r'^(?:G0?([123]))\*')
+
+        # Single G74 or multi G75 quadrant for circular interpolation
+        self.quad_re = re.compile(r'^G7([45]).*\*$')
+
+        # Region mode on
+        # In region mode, D01 starts a region
+        # and D02 ends it. A new region can be started again
+        # with D01. All contours must be closed before
+        # D02 or G37.
+        self.regionon_re = re.compile(r'^G36\*$')
+
+        # Region mode off
+        # Will end a region and come off region mode.
+        # All contours must be closed before D02 or G37.
+        self.regionoff_re = re.compile(r'^G37\*$')
+
+        # End of file
+        self.eof_re = re.compile(r'^M02\*')
+
+        # IP - Image polarity
+        self.pol_re = re.compile(r'^%?IP(POS|NEG)\*%?$')
+
+        # LP - Level polarity
+        self.lpol_re = re.compile(r'^%LP([DC])\*%$')
+
+        # Units (OBSOLETE)
+        self.units_re = re.compile(r'^G7([01])\*$')
+
+        # Absolute/Relative G90/1 (OBSOLETE)
+        self.absrel_re = re.compile(r'^G9([01])\*$')
+
+        # Aperture macros
+        self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
+        self.am2_re = re.compile(r'(.*)%$')
+
+        self.use_buffer_for_union = self.app.defaults["gerber_use_buffer_for_union"]
+
+    def aperture_parse(self, apertureId, apertureType, apParameters):
+        """
+        Parse gerber aperture definition into dictionary of apertures.
+        The following kinds and their attributes are supported:
+
+        * *Circular (C)*: size (float)
+        * *Rectangle (R)*: width (float), height (float)
+        * *Obround (O)*: width (float), height (float).
+        * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
+        * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
+
+        :param apertureId: Id of the aperture being defined.
+        :param apertureType: Type of the aperture.
+        :param apParameters: Parameters of the aperture.
+        :type apertureId: str
+        :type apertureType: str
+        :type apParameters: str
+        :return: Identifier of the aperture.
+        :rtype: str
+        """
+        if self.app.abort_flag:
+            # graceful abort requested by the user
+            raise FlatCAMApp.GracefulException
+
+        # Found some Gerber with a leading zero in the aperture id and the
+        # referenced it without the zero, so this is a hack to handle that.
+        apid = str(int(apertureId))
+
+        try:  # Could be empty for aperture macros
+            paramList = apParameters.split('X')
+        except:
+            paramList = None
+
+        if apertureType == "C":  # Circle, example: %ADD11C,0.1*%
+            self.apertures[apid] = {"type": "C",
+                                    "size": float(paramList[0])}
+            return apid
+
+        if apertureType == "R":  # Rectangle, example: %ADD15R,0.05X0.12*%
+            self.apertures[apid] = {"type": "R",
+                                    "width": float(paramList[0]),
+                                    "height": float(paramList[1]),
+                                    "size": sqrt(float(paramList[0]) ** 2 + float(paramList[1]) ** 2)}  # Hack
+            return apid
+
+        if apertureType == "O":  # Obround
+            self.apertures[apid] = {"type": "O",
+                                    "width": float(paramList[0]),
+                                    "height": float(paramList[1]),
+                                    "size": sqrt(float(paramList[0]) ** 2 + float(paramList[1]) ** 2)}  # Hack
+            return apid
+
+        if apertureType == "P":  # Polygon (regular)
+            self.apertures[apid] = {"type": "P",
+                                    "diam": float(paramList[0]),
+                                    "nVertices": int(paramList[1]),
+                                    "size": float(paramList[0])}  # Hack
+            if len(paramList) >= 3:
+                self.apertures[apid]["rotation"] = float(paramList[2])
+            return apid
+
+        if apertureType in self.aperture_macros:
+            self.apertures[apid] = {"type": "AM",
+                                    "macro": self.aperture_macros[apertureType],
+                                    "modifiers": paramList}
+            return apid
+
+        log.warning("Aperture not implemented: %s" % str(apertureType))
+        return None
+
+    def parse_file(self, filename, follow=False):
+        """
+        Calls Gerber.parse_lines() with generator of lines
+        read from the given file. Will split the lines if multiple
+        statements are found in a single original line.
+
+        The following line is split into two::
+
+            G54D11*G36*
+
+        First is ``G54D11*`` and seconds is ``G36*``.
+
+        :param filename: Gerber file to parse.
+        :type filename: str
+        :param follow: If true, will not create polygons, just lines
+            following the gerber path.
+        :type follow: bool
+        :return: None
+        """
+
+        with open(filename, 'r') as gfile:
+
+            def line_generator():
+                for line in gfile:
+                    line = line.strip(' \r\n')
+                    while len(line) > 0:
+
+                        # If ends with '%' leave as is.
+                        if line[-1] == '%':
+                            yield line
+                            break
+
+                        # Split after '*' if any.
+                        starpos = line.find('*')
+                        if starpos > -1:
+                            cleanline = line[:starpos + 1]
+                            yield cleanline
+                            line = line[starpos + 1:]
+
+                        # Otherwise leave as is.
+                        else:
+                            # yield clean line
+                            yield line
+                            break
+
+            processed_lines = list(line_generator())
+            self.parse_lines(processed_lines)
+
+    # @profile
+    def parse_lines(self, glines):
+        """
+        Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
+        ``self.flashes``, ``self.regions`` and ``self.units``.
+
+        :param glines: Gerber code as list of strings, each element being
+            one line of the source file.
+        :type glines: list
+        :return: None
+        :rtype: None
+        """
+
+        # Coordinates of the current path, each is [x, y]
+        path = []
+
+        # this is for temporary storage of solid geometry until it is added to poly_buffer
+        geo_s = None
+
+        # this is for temporary storage of follow geometry until it is added to follow_buffer
+        geo_f = None
+
+        # Polygons are stored here until there is a change in polarity.
+        # Only then they are combined via cascaded_union and added or
+        # subtracted from solid_geometry. This is ~100 times faster than
+        # applying a union for every new polygon.
+        poly_buffer = []
+
+        # store here the follow geometry
+        follow_buffer = []
+
+        last_path_aperture = None
+        current_aperture = None
+
+        # 1,2 or 3 from "G01", "G02" or "G03"
+        current_interpolation_mode = None
+
+        # 1 or 2 from "D01" or "D02"
+        # Note this is to support deprecated Gerber not putting
+        # an operation code at the end of every coordinate line.
+        current_operation_code = None
+
+        # Current coordinates
+        current_x = None
+        current_y = None
+        previous_x = None
+        previous_y = None
+
+        current_d = None
+
+        # Absolute or Relative/Incremental coordinates
+        # Not implemented
+        absolute = True
+
+        # How to interpret circular interpolation: SINGLE or MULTI
+        quadrant_mode = None
+
+        # Indicates we are parsing an aperture macro
+        current_macro = None
+
+        # Indicates the current polarity: D-Dark, C-Clear
+        current_polarity = 'D'
+
+        # If a region is being defined
+        making_region = False
+
+        # ### Parsing starts here ## ##
+        line_num = 0
+        gline = ""
+
+        s_tol = float(self.app.defaults["gerber_simp_tolerance"])
+
+        self.app.inform.emit('%s %d %s.' % (_("Gerber processing. Parsing"), len(glines), _("lines")))
+        try:
+            for gline in glines:
+                if self.app.abort_flag:
+                    # graceful abort requested by the user
+                    raise FlatCAMApp.GracefulException
+
+                line_num += 1
+                self.source_file += gline + '\n'
+
+                # Cleanup #
+                gline = gline.strip(' \r\n')
+                # log.debug("Line=%3s %s" % (line_num, gline))
+
+                # ###################
+                # Ignored lines #####
+                # Comments      #####
+                # ###################
+                match = self.comm_re.search(gline)
+                if match:
+                    continue
+
+                # Polarity change ###### ##
+                # Example: %LPD*% or %LPC*%
+                # If polarity changes, creates geometry from current
+                # buffer, then adds or subtracts accordingly.
+                match = self.lpol_re.search(gline)
+                if match:
+                    new_polarity = match.group(1)
+                    # log.info("Polarity CHANGE, LPC = %s, poly_buff = %s" % (self.is_lpc, poly_buffer))
+                    self.is_lpc = True if new_polarity == 'C' else False
+                    if len(path) > 1 and current_polarity != new_polarity:
+
+                        # finish the current path and add it to the storage
+                        # --- Buffered ----
+                        width = self.apertures[last_path_aperture]["size"]
+
+                        geo_dict = dict()
+                        geo_f = LineString(path)
+                        if not geo_f.is_empty:
+                            follow_buffer.append(geo_f)
+                            geo_dict['follow'] = geo_f
+
+                        geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                        if not geo_s.is_empty:
+                            if self.app.defaults['gerber_simplification']:
+                                poly_buffer.append(geo_s.simplify(s_tol))
+                            else:
+                                poly_buffer.append(geo_s)
+                            if self.is_lpc is True:
+                                geo_dict['clear'] = geo_s
+                            else:
+                                geo_dict['solid'] = geo_s
+
+                        if last_path_aperture not in self.apertures:
+                            self.apertures[last_path_aperture] = dict()
+                        if 'geometry' not in self.apertures[last_path_aperture]:
+                            self.apertures[last_path_aperture]['geometry'] = []
+                        self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        path = [path[-1]]
+
+                    # --- Apply buffer ---
+                    # If added for testing of bug #83
+                    # TODO: Remove when bug fixed
+                    if len(poly_buffer) > 0:
+                        if current_polarity == 'D':
+                            # self.follow_geometry = self.follow_geometry.union(cascaded_union(follow_buffer))
+                            self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
+
+                        else:
+                            # self.follow_geometry = self.follow_geometry.difference(cascaded_union(follow_buffer))
+                            self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
+
+                        # follow_buffer = []
+                        poly_buffer = []
+
+                    current_polarity = new_polarity
+                    continue
+
+                # ############################################################# ##
+                # Number format ############################################### ##
+                # Example: %FSLAX24Y24*%
+                # ############################################################# ##
+                # TODO: This is ignoring most of the format. Implement the rest.
+                match = self.fmt_re.search(gline)
+                if match:
+                    absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
+                    if match.group(1) is not None:
+                        self.gerber_zeros = match.group(1)
+                    self.int_digits = int(match.group(3))
+                    self.frac_digits = int(match.group(4))
+                    log.debug("Gerber format found. (%s) " % str(gline))
+
+                    log.debug(
+                        "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
+                        "D-no zero supression)" % self.gerber_zeros)
+                    log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
+                    continue
+
+                # ## Mode (IN/MM)
+                # Example: %MOIN*%
+                match = self.mode_re.search(gline)
+                if match:
+                    self.gerber_units = match.group(1)
+                    log.debug("Gerber units found = %s" % self.gerber_units)
+                    # Changed for issue #80
+                    self.convert_units(match.group(1))
+                    continue
+
+                # ############################################################# ##
+                # Combined Number format and Mode --- Allegro does this ####### ##
+                # ############################################################# ##
+                match = self.fmt_re_alt.search(gline)
+                if match:
+                    absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(2)]
+                    if match.group(1) is not None:
+                        self.gerber_zeros = match.group(1)
+                    self.int_digits = int(match.group(3))
+                    self.frac_digits = int(match.group(4))
+                    log.debug("Gerber format found. (%s) " % str(gline))
+                    log.debug(
+                        "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
+                        "D-no zero suppression)" % self.gerber_zeros)
+                    log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
+
+                    self.gerber_units = match.group(5)
+                    log.debug("Gerber units found = %s" % self.gerber_units)
+                    # Changed for issue #80
+                    self.convert_units(match.group(5))
+                    continue
+
+                # ############################################################# ##
+                # Search for OrCAD way for having Number format
+                # ############################################################# ##
+                match = self.fmt_re_orcad.search(gline)
+                if match:
+                    if match.group(1) is not None:
+                        if match.group(1) == 'G74':
+                            quadrant_mode = 'SINGLE'
+                        elif match.group(1) == 'G75':
+                            quadrant_mode = 'MULTI'
+                        absolute = {'A': 'Absolute', 'I': 'Relative'}[match.group(3)]
+                        if match.group(2) is not None:
+                            self.gerber_zeros = match.group(2)
+
+                        self.int_digits = int(match.group(4))
+                        self.frac_digits = int(match.group(5))
+                        log.debug("Gerber format found. (%s) " % str(gline))
+                        log.debug(
+                            "Gerber format found. Gerber zeros = %s (L-omit leading zeros, T-omit trailing zeros, "
+                            "D-no zerosuppressionn)" % self.gerber_zeros)
+                        log.debug("Gerber format found. Coordinates type = %s (Absolute or Relative)" % absolute)
+
+                        self.gerber_units = match.group(1)
+                        log.debug("Gerber units found = %s" % self.gerber_units)
+                        # Changed for issue #80
+                        self.convert_units(match.group(5))
+                        continue
+
+                # ############################################################# ##
+                # Units (G70/1) OBSOLETE
+                # ############################################################# ##
+                match = self.units_re.search(gline)
+                if match:
+                    obs_gerber_units = {'0': 'IN', '1': 'MM'}[match.group(1)]
+                    log.warning("Gerber obsolete units found = %s" % obs_gerber_units)
+                    # Changed for issue #80
+                    self.convert_units({'0': 'IN', '1': 'MM'}[match.group(1)])
+                    continue
+
+                # ############################################################# ##
+                # Absolute/relative coordinates G90/1 OBSOLETE ######## ##
+                # ##################################################### ##
+                match = self.absrel_re.search(gline)
+                if match:
+                    absolute = {'0': "Absolute", '1': "Relative"}[match.group(1)]
+                    log.warning("Gerber obsolete coordinates type found = %s (Absolute or Relative) " % absolute)
+                    continue
+
+                # ############################################################# ##
+                # Aperture Macros ##################################### ##
+                # Having this at the beginning will slow things down
+                # but macros can have complicated statements than could
+                # be caught by other patterns.
+                # ############################################################# ##
+                if current_macro is None:  # No macro started yet
+                    match = self.am1_re.search(gline)
+                    # Start macro if match, else not an AM, carry on.
+                    if match:
+                        log.debug("Starting macro. Line %d: %s" % (line_num, gline))
+                        current_macro = match.group(1)
+                        self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
+                        if match.group(2):  # Append
+                            self.aperture_macros[current_macro].append(match.group(2))
+                        if match.group(3):  # Finish macro
+                            # self.aperture_macros[current_macro].parse_content()
+                            current_macro = None
+                            log.debug("Macro complete in 1 line.")
+                        continue
+                else:  # Continue macro
+                    log.debug("Continuing macro. Line %d." % line_num)
+                    match = self.am2_re.search(gline)
+                    if match:  # Finish macro
+                        log.debug("End of macro. Line %d." % line_num)
+                        self.aperture_macros[current_macro].append(match.group(1))
+                        # self.aperture_macros[current_macro].parse_content()
+                        current_macro = None
+                    else:  # Append
+                        self.aperture_macros[current_macro].append(gline)
+                    continue
+
+                # ## Aperture definitions %ADD...
+                match = self.ad_re.search(gline)
+                if match:
+                    # log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
+                    self.aperture_parse(match.group(1), match.group(2), match.group(3))
+                    continue
+
+                # ############################################################# ##
+                # Operation code alone ###################### ##
+                # Operation code alone, usually just D03 (Flash)
+                # self.opcode_re = re.compile(r'^D0?([123])\*$')
+                # ############################################################# ##
+                match = self.opcode_re.search(gline)
+                if match:
+                    current_operation_code = int(match.group(1))
+                    current_d = current_operation_code
+
+                    if current_operation_code == 3:
+
+                        # --- Buffered ---
+                        try:
+                            log.debug("Bare op-code %d." % current_operation_code)
+                            geo_dict = dict()
+                            flash = self.create_flash_geometry(
+                                Point(current_x, current_y), self.apertures[current_aperture],
+                                self.steps_per_circle)
+
+                            geo_dict['follow'] = Point([current_x, current_y])
+
+                            if not flash.is_empty:
+                                if self.app.defaults['gerber_simplification']:
+                                    poly_buffer.append(flash.simplify(s_tol))
+                                else:
+                                    poly_buffer.append(flash)
+                                if self.is_lpc is True:
+                                    geo_dict['clear'] = flash
+                                else:
+                                    geo_dict['solid'] = flash
+
+                                if current_aperture not in self.apertures:
+                                    self.apertures[current_aperture] = dict()
+                                if 'geometry' not in self.apertures[current_aperture]:
+                                    self.apertures[current_aperture]['geometry'] = []
+                                self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        except IndexError:
+                            log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
+
+                    continue
+
+                # ############################################################# ##
+                # Tool/aperture change
+                # Example: D12*
+                # ############################################################# ##
+                match = self.tool_re.search(gline)
+                if match:
+                    current_aperture = match.group(1)
+                    # log.debug("Line %d: Aperture change to (%s)" % (line_num, current_aperture))
+
+                    # If the aperture value is zero then make it something quite small but with a non-zero value
+                    # so it can be processed by FlatCAM.
+                    # But first test to see if the aperture type is "aperture macro". In that case
+                    # we should not test for "size" key as it does not exist in this case.
+                    if self.apertures[current_aperture]["type"] is not "AM":
+                        if self.apertures[current_aperture]["size"] == 0:
+                            self.apertures[current_aperture]["size"] = 1e-12
+                    # log.debug(self.apertures[current_aperture])
+
+                    # Take care of the current path with the previous tool
+                    if len(path) > 1:
+                        if self.apertures[last_path_aperture]["type"] == 'R':
+                            # do nothing because 'R' type moving aperture is none at once
+                            pass
+                        else:
+                            geo_dict = dict()
+                            geo_f = LineString(path)
+                            if not geo_f.is_empty:
+                                follow_buffer.append(geo_f)
+                                geo_dict['follow'] = geo_f
+
+                            # --- Buffered ----
+                            width = self.apertures[last_path_aperture]["size"]
+                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            if not geo_s.is_empty:
+                                if self.app.defaults['gerber_simplification']:
+                                    poly_buffer.append(geo_s.simplify(s_tol))
+                                else:
+                                    poly_buffer.append(geo_s)
+                                if self.is_lpc is True:
+                                    geo_dict['clear'] = geo_s
+                                else:
+                                    geo_dict['solid'] = geo_s
+
+                            if last_path_aperture not in self.apertures:
+                                self.apertures[last_path_aperture] = dict()
+                            if 'geometry' not in self.apertures[last_path_aperture]:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                            path = [path[-1]]
+
+                    continue
+
+                # ############################################################# ##
+                # G36* - Begin region
+                # ############################################################# ##
+                if self.regionon_re.search(gline):
+                    if len(path) > 1:
+                        # Take care of what is left in the path
+
+                        geo_dict = dict()
+                        geo_f = LineString(path)
+                        if not geo_f.is_empty:
+                            follow_buffer.append(geo_f)
+                            geo_dict['follow'] = geo_f
+
+                        # --- Buffered ----
+                        width = self.apertures[last_path_aperture]["size"]
+                        geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                        if not geo_s.is_empty:
+                            if self.app.defaults['gerber_simplification']:
+                                poly_buffer.append(geo_s.simplify(s_tol))
+                            else:
+                                poly_buffer.append(geo_s)
+                            if self.is_lpc is True:
+                                geo_dict['clear'] = geo_s
+                            else:
+                                geo_dict['solid'] = geo_s
+
+                        if last_path_aperture not in self.apertures:
+                            self.apertures[last_path_aperture] = dict()
+                        if 'geometry' not in self.apertures[last_path_aperture]:
+                            self.apertures[last_path_aperture]['geometry'] = []
+                        self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        path = [path[-1]]
+
+                    making_region = True
+                    continue
+
+                # ############################################################# ##
+                # G37* - End region
+                # ############################################################# ##
+                if self.regionoff_re.search(gline):
+                    making_region = False
+
+                    if '0' not in self.apertures:
+                        self.apertures['0'] = {}
+                        self.apertures['0']['type'] = 'REG'
+                        self.apertures['0']['size'] = 0.0
+                        self.apertures['0']['geometry'] = []
+
+                    # if D02 happened before G37 we now have a path with 1 element only; we have to add the current
+                    # geo to the poly_buffer otherwise we loose it
+                    if current_operation_code == 2:
+                        if len(path) == 1:
+                            # this means that the geometry was prepared previously and we just need to add it
+                            geo_dict = dict()
+                            if geo_f:
+                                if not geo_f.is_empty:
+                                    follow_buffer.append(geo_f)
+                                    geo_dict['follow'] = geo_f
+                            if geo_s:
+                                if not geo_s.is_empty:
+                                    if self.app.defaults['gerber_simplification']:
+                                        poly_buffer.append(geo_s.simplify(s_tol))
+                                    else:
+                                        poly_buffer.append(geo_s)
+                                    if self.is_lpc is True:
+                                        geo_dict['clear'] = geo_s
+                                    else:
+                                        geo_dict['solid'] = geo_s
+
+                            if geo_s or geo_f:
+                                self.apertures['0']['geometry'].append(deepcopy(geo_dict))
+
+                            path = [[current_x, current_y]]  # Start new path
+
+                    # Only one path defines region?
+                    # This can happen if D02 happened before G37 and
+                    # is not and error.
+                    if len(path) < 3:
+                        # print "ERROR: Path contains less than 3 points:"
+                        # path = [[current_x, current_y]]
+                        continue
+
+                    # For regions we may ignore an aperture that is None
+
+                    # --- Buffered ---
+                    geo_dict = dict()
+                    region_f = Polygon(path).exterior
+                    if not region_f.is_empty:
+                        follow_buffer.append(region_f)
+                        geo_dict['follow'] = region_f
+
+                    region_s = Polygon(path)
+                    if not region_s.is_valid:
+                        region_s = region_s.buffer(0, int(self.steps_per_circle / 4))
+
+                    if not region_s.is_empty:
+                        if self.app.defaults['gerber_simplification']:
+                            poly_buffer.append(region_s.simplify(s_tol))
+                        else:
+                            poly_buffer.append(region_s)
+                        if self.is_lpc is True:
+                            geo_dict['clear'] = region_s
+                        else:
+                            geo_dict['solid'] = region_s
+
+                    if not region_s.is_empty or not region_f.is_empty:
+                        self.apertures['0']['geometry'].append(deepcopy(geo_dict))
+
+                    path = [[current_x, current_y]]  # Start new path
+                    continue
+
+                # ## G01/2/3* - Interpolation mode change
+                # Can occur along with coordinates and operation code but
+                # sometimes by itself (handled here).
+                # Example: G01*
+                match = self.interp_re.search(gline)
+                if match:
+                    current_interpolation_mode = int(match.group(1))
+                    continue
+
+                # ## G01 - Linear interpolation plus flashes
+                # Operation code (D0x) missing is deprecated... oh well I will support it.
+                # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
+                match = self.lin_re.search(gline)
+                if match:
+                    # Dxx alone?
+                    # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
+                    #     try:
+                    #         current_operation_code = int(match.group(4))
+                    #     except:
+                    #         pass  # A line with just * will match too.
+                    #     continue
+                    # NOTE: Letting it continue allows it to react to the
+                    #       operation code.
+
+                    # Parse coordinates
+                    if match.group(2) is not None:
+                        linear_x = parse_gerber_number(match.group(2),
+                                                       self.int_digits, self.frac_digits, self.gerber_zeros)
+                        current_x = linear_x
+                    else:
+                        linear_x = current_x
+                    if match.group(3) is not None:
+                        linear_y = parse_gerber_number(match.group(3),
+                                                       self.int_digits, self.frac_digits, self.gerber_zeros)
+                        current_y = linear_y
+                    else:
+                        linear_y = current_y
+
+                    # Parse operation code
+                    if match.group(4) is not None:
+                        current_operation_code = int(match.group(4))
+
+                    # Pen down: add segment
+                    if current_operation_code == 1:
+                        # if linear_x or linear_y are None, ignore those
+                        if current_x is not None and current_y is not None:
+                            # only add the point if it's a new one otherwise skip it (harder to process)
+                            if path[-1] != [current_x, current_y]:
+                                path.append([current_x, current_y])
+
+                            if making_region is False:
+                                # if the aperture is rectangle then add a rectangular shape having as parameters the
+                                # coordinates of the start and end point and also the width and height
+                                # of the 'R' aperture
+                                try:
+                                    if self.apertures[current_aperture]["type"] == 'R':
+                                        width = self.apertures[current_aperture]['width']
+                                        height = self.apertures[current_aperture]['height']
+                                        minx = min(path[0][0], path[1][0]) - width / 2
+                                        maxx = max(path[0][0], path[1][0]) + width / 2
+                                        miny = min(path[0][1], path[1][1]) - height / 2
+                                        maxy = max(path[0][1], path[1][1]) + height / 2
+                                        log.debug("Coords: %s - %s - %s - %s" % (minx, miny, maxx, maxy))
+
+                                        geo_dict = dict()
+                                        geo_f = Point([current_x, current_y])
+                                        follow_buffer.append(geo_f)
+                                        geo_dict['follow'] = geo_f
+
+                                        geo_s = shply_box(minx, miny, maxx, maxy)
+                                        if self.app.defaults['gerber_simplification']:
+                                            poly_buffer.append(geo_s.simplify(s_tol))
+                                        else:
+                                            poly_buffer.append(geo_s)
+
+                                        if self.is_lpc is True:
+                                            geo_dict['clear'] = geo_s
+                                        else:
+                                            geo_dict['solid'] = geo_s
+
+                                        if current_aperture not in self.apertures:
+                                            self.apertures[current_aperture] = dict()
+                                        if 'geometry' not in self.apertures[current_aperture]:
+                                            self.apertures[current_aperture]['geometry'] = []
+                                        self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
+                                except Exception as e:
+                                    pass
+                            last_path_aperture = current_aperture
+                            # we do this for the case that a region is done without having defined any aperture
+                            if last_path_aperture is None:
+                                if '0' not in self.apertures:
+                                    self.apertures['0'] = {}
+                                    self.apertures['0']['type'] = 'REG'
+                                    self.apertures['0']['size'] = 0.0
+                                    self.apertures['0']['geometry'] = []
+                                last_path_aperture = '0'
+                        else:
+                            self.app.inform.emit('[WARNING] %s: %s' %
+                                                 (_("Coordinates missing, line ignored"), str(gline)))
+                            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                                 _("GERBER file might be CORRUPT. Check the file !!!"))
+
+                    elif current_operation_code == 2:
+                        if len(path) > 1:
+                            geo_s = None
+                            geo_f = None
+
+                            geo_dict = dict()
+                            # --- BUFFERED ---
+                            # this treats the case when we are storing geometry as paths only
+                            if making_region:
+                                # we do this for the case that a region is done without having defined any aperture
+                                if last_path_aperture is None:
+                                    if '0' not in self.apertures:
+                                        self.apertures['0'] = {}
+                                        self.apertures['0']['type'] = 'REG'
+                                        self.apertures['0']['size'] = 0.0
+                                        self.apertures['0']['geometry'] = []
+                                    last_path_aperture = '0'
+                                geo_f = Polygon()
+                            else:
+                                geo_f = LineString(path)
+
+                            try:
+                                if self.apertures[last_path_aperture]["type"] != 'R':
+                                    if not geo_f.is_empty:
+                                        follow_buffer.append(geo_f)
+                                        geo_dict['follow'] = geo_f
+                            except Exception as e:
+                                log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
+                                if not geo_f.is_empty:
+                                    follow_buffer.append(geo_f)
+                                    geo_dict['follow'] = geo_f
+
+                            # this treats the case when we are storing geometry as solids
+                            if making_region:
+                                # we do this for the case that a region is done without having defined any aperture
+                                if last_path_aperture is None:
+                                    if '0' not in self.apertures:
+                                        self.apertures['0'] = {}
+                                        self.apertures['0']['type'] = 'REG'
+                                        self.apertures['0']['size'] = 0.0
+                                        self.apertures['0']['geometry'] = []
+                                    last_path_aperture = '0'
+
+                                try:
+                                    geo_s = Polygon(path)
+                                except ValueError:
+                                    log.warning("Problem %s %s" % (gline, line_num))
+                                    self.app.inform.emit('[ERROR] %s: %s' %
+                                                         (_("Region does not have enough points. "
+                                                            "File will be processed but there are parser errors. "
+                                                            "Line number"), str(line_num)))
+                            else:
+                                if last_path_aperture is None:
+                                    log.warning("No aperture defined for curent path. (%d)" % line_num)
+                                width = self.apertures[last_path_aperture]["size"]  # TODO: WARNING this should fail!
+                                geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+
+                            try:
+                                if self.apertures[last_path_aperture]["type"] != 'R':
+                                    if not geo_s.is_empty:
+                                        if self.app.defaults['gerber_simplification']:
+                                            poly_buffer.append(geo_s.simplify(s_tol))
+                                        else:
+                                            poly_buffer.append(geo_s)
+
+                                        if self.is_lpc is True:
+                                            geo_dict['clear'] = geo_s
+                                        else:
+                                            geo_dict['solid'] = geo_s
+                            except Exception as e:
+                                log.debug("camlib.Gerber.parse_lines() --> %s" % str(e))
+                                if self.app.defaults['gerber_simplification']:
+                                    poly_buffer.append(geo_s.simplify(s_tol))
+                                else:
+                                    poly_buffer.append(geo_s)
+
+                                if self.is_lpc is True:
+                                    geo_dict['clear'] = geo_s
+                                else:
+                                    geo_dict['solid'] = geo_s
+
+                            if last_path_aperture not in self.apertures:
+                                self.apertures[last_path_aperture] = dict()
+                            if 'geometry' not in self.apertures[last_path_aperture]:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        # if linear_x or linear_y are None, ignore those
+                        if linear_x is not None and linear_y is not None:
+                            path = [[linear_x, linear_y]]  # Start new path
+                        else:
+                            self.app.inform.emit('[WARNING] %s: %s' %
+                                                 (_("Coordinates missing, line ignored"), str(gline)))
+                            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                                 _("GERBER file might be CORRUPT. Check the file !!!"))
+
+                    # Flash
+                    # Not allowed in region mode.
+                    elif current_operation_code == 3:
+
+                        # Create path draw so far.
+                        if len(path) > 1:
+                            # --- Buffered ----
+                            geo_dict = dict()
+
+                            # this treats the case when we are storing geometry as paths
+                            geo_f = LineString(path)
+                            if not geo_f.is_empty:
+                                try:
+                                    if self.apertures[last_path_aperture]["type"] != 'R':
+                                        follow_buffer.append(geo_f)
+                                        geo_dict['follow'] = geo_f
+                                except Exception as e:
+                                    log.debug("camlib.Gerber.parse_lines() --> G01 match D03 --> %s" % str(e))
+                                    follow_buffer.append(geo_f)
+                                    geo_dict['follow'] = geo_f
+
+                            # this treats the case when we are storing geometry as solids
+                            width = self.apertures[last_path_aperture]["size"]
+                            geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                            if not geo_s.is_empty:
+                                try:
+                                    if self.apertures[last_path_aperture]["type"] != 'R':
+                                        if self.app.defaults['gerber_simplification']:
+                                            poly_buffer.append(geo_s.simplify(s_tol))
+                                        else:
+                                            poly_buffer.append(geo_s)
+
+                                        if self.is_lpc is True:
+                                            geo_dict['clear'] = geo_s
+                                        else:
+                                            geo_dict['solid'] = geo_s
+                                except:
+                                    if self.app.defaults['gerber_simplification']:
+                                        poly_buffer.append(geo_s.simplify(s_tol))
+                                    else:
+                                        poly_buffer.append(geo_s)
+
+                                    if self.is_lpc is True:
+                                        geo_dict['clear'] = geo_s
+                                    else:
+                                        geo_dict['solid'] = geo_s
+
+                            if last_path_aperture not in self.apertures:
+                                self.apertures[last_path_aperture] = dict()
+                            if 'geometry' not in self.apertures[last_path_aperture]:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        # Reset path starting point
+                        path = [[linear_x, linear_y]]
+
+                        # --- BUFFERED ---
+                        # Draw the flash
+                        # this treats the case when we are storing geometry as paths
+                        geo_dict = dict()
+                        geo_flash = Point([linear_x, linear_y])
+                        follow_buffer.append(geo_flash)
+                        geo_dict['follow'] = geo_flash
+
+                        # this treats the case when we are storing geometry as solids
+                        flash = self.create_flash_geometry(
+                            Point([linear_x, linear_y]),
+                            self.apertures[current_aperture],
+                            self.steps_per_circle
+                        )
+                        if not flash.is_empty:
+                            if self.app.defaults['gerber_simplification']:
+                                poly_buffer.append(flash.simplify(s_tol))
+                            else:
+                                poly_buffer.append(flash)
+
+                            if self.is_lpc is True:
+                                geo_dict['clear'] = flash
+                            else:
+                                geo_dict['solid'] = flash
+
+                        if current_aperture not in self.apertures:
+                            self.apertures[current_aperture] = dict()
+                        if 'geometry' not in self.apertures[current_aperture]:
+                            self.apertures[current_aperture]['geometry'] = []
+                        self.apertures[current_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                    # maybe those lines are not exactly needed but it is easier to read the program as those coordinates
+                    # are used in case that circular interpolation is encountered within the Gerber file
+                    current_x = linear_x
+                    current_y = linear_y
+
+                    # log.debug("Line_number=%3s X=%s Y=%s (%s)" % (line_num, linear_x, linear_y, gline))
+                    continue
+
+                # ## G74/75* - Single or multiple quadrant arcs
+                match = self.quad_re.search(gline)
+                if match:
+                    if match.group(1) == '4':
+                        quadrant_mode = 'SINGLE'
+                    else:
+                        quadrant_mode = 'MULTI'
+                    continue
+
+                # ## G02/3 - Circular interpolation
+                # 2-clockwise, 3-counterclockwise
+                # Ex. format: G03 X0 Y50 I-50 J0 where the X, Y coords are the coords of the End Point
+                match = self.circ_re.search(gline)
+                if match:
+                    arcdir = [None, None, "cw", "ccw"]
+
+                    mode, circular_x, circular_y, i, j, d = match.groups()
+
+                    try:
+                        circular_x = parse_gerber_number(circular_x,
+                                                         self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except:
+                        circular_x = current_x
+
+                    try:
+                        circular_y = parse_gerber_number(circular_y,
+                                                         self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except:
+                        circular_y = current_y
+
+                    # According to Gerber specification i and j are not modal, which means that when i or j are missing,
+                    # they are to be interpreted as being zero
+                    try:
+                        i = parse_gerber_number(i, self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except:
+                        i = 0
+
+                    try:
+                        j = parse_gerber_number(j, self.int_digits, self.frac_digits, self.gerber_zeros)
+                    except:
+                        j = 0
+
+                    if quadrant_mode is None:
+                        log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
+                        log.error(gline)
+                        continue
+
+                    if mode is None and current_interpolation_mode not in [2, 3]:
+                        log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
+                        log.error(gline)
+                        continue
+                    elif mode is not None:
+                        current_interpolation_mode = int(mode)
+
+                    # Set operation code if provided
+                    if d is not None:
+                        current_operation_code = int(d)
+
+                    # Nothing created! Pen Up.
+                    if current_operation_code == 2:
+                        log.warning("Arc with D2. (%d)" % line_num)
+                        if len(path) > 1:
+                            geo_dict = dict()
+
+                            if last_path_aperture is None:
+                                log.warning("No aperture defined for curent path. (%d)" % line_num)
+
+                            # --- BUFFERED ---
+                            width = self.apertures[last_path_aperture]["size"]
+
+                            # this treats the case when we are storing geometry as paths
+                            geo_f = LineString(path)
+                            if not geo_f.is_empty:
+                                follow_buffer.append(geo_f)
+                                geo_dict['follow'] = geo_f
+
+                            # this treats the case when we are storing geometry as solids
+                            buffered = LineString(path).buffer(width / 1.999, int(self.steps_per_circle))
+                            if not buffered.is_empty:
+                                if self.app.defaults['gerber_simplification']:
+                                    poly_buffer.append(buffered.simplify(s_tol))
+                                else:
+                                    poly_buffer.append(buffered)
+
+                                if self.is_lpc is True:
+                                    geo_dict['clear'] = buffered
+                                else:
+                                    geo_dict['solid'] = buffered
+
+                            if last_path_aperture not in self.apertures:
+                                self.apertures[last_path_aperture] = dict()
+                            if 'geometry' not in self.apertures[last_path_aperture]:
+                                self.apertures[last_path_aperture]['geometry'] = []
+                            self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+                        current_x = circular_x
+                        current_y = circular_y
+                        path = [[current_x, current_y]]  # Start new path
+                        continue
+
+                    # Flash should not happen here
+                    if current_operation_code == 3:
+                        log.error("Trying to flash within arc. (%d)" % line_num)
+                        continue
+
+                    if quadrant_mode == 'MULTI':
+                        center = [i + current_x, j + current_y]
+                        radius = sqrt(i ** 2 + j ** 2)
+                        start = arctan2(-j, -i)  # Start angle
+                        # Numerical errors might prevent start == stop therefore
+                        # we check ahead of time. This should result in a
+                        # 360 degree arc.
+                        if current_x == circular_x and current_y == circular_y:
+                            stop = start
+                        else:
+                            stop = arctan2(-center[1] + circular_y, -center[0] + circular_x)  # Stop angle
+
+                        this_arc = arc(center, radius, start, stop,
+                                       arcdir[current_interpolation_mode],
+                                       self.steps_per_circle)
+
+                        # The last point in the computed arc can have
+                        # numerical errors. The exact final point is the
+                        # specified (x, y). Replace.
+                        this_arc[-1] = (circular_x, circular_y)
+
+                        # Last point in path is current point
+                        # current_x = this_arc[-1][0]
+                        # current_y = this_arc[-1][1]
+                        current_x, current_y = circular_x, circular_y
+
+                        # Append
+                        path += this_arc
+                        last_path_aperture = current_aperture
+
+                        continue
+
+                    if quadrant_mode == 'SINGLE':
+
+                        center_candidates = [
+                            [i + current_x, j + current_y],
+                            [-i + current_x, j + current_y],
+                            [i + current_x, -j + current_y],
+                            [-i + current_x, -j + current_y]
+                        ]
+
+                        valid = False
+                        log.debug("I: %f  J: %f" % (i, j))
+                        for center in center_candidates:
+                            radius = sqrt(i ** 2 + j ** 2)
+
+                            # Make sure radius to start is the same as radius to end.
+                            radius2 = sqrt((center[0] - circular_x) ** 2 + (center[1] - circular_y) ** 2)
+                            if radius2 < radius * 0.95 or radius2 > radius * 1.05:
+                                continue  # Not a valid center.
+
+                            # Correct i and j and continue as with multi-quadrant.
+                            i = center[0] - current_x
+                            j = center[1] - current_y
+
+                            start = arctan2(-j, -i)  # Start angle
+                            stop = arctan2(-center[1] + circular_y, -center[0] + circular_x)  # Stop angle
+                            angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
+                            log.debug("ARC START: %f, %f  CENTER: %f, %f  STOP: %f, %f" %
+                                      (current_x, current_y, center[0], center[1], circular_x, circular_y))
+                            log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
+                                      (start * 180 / pi, stop * 180 / pi, arcdir[current_interpolation_mode],
+                                       angle * 180 / pi, pi / 2 * 180 / pi, angle <= (pi + 1e-6) / 2))
+
+                            if angle <= (pi + 1e-6) / 2:
+                                log.debug("########## ACCEPTING ARC ############")
+                                this_arc = arc(center, radius, start, stop,
+                                               arcdir[current_interpolation_mode],
+                                               self.steps_per_circle)
+
+                                # Replace with exact values
+                                this_arc[-1] = (circular_x, circular_y)
+
+                                # current_x = this_arc[-1][0]
+                                # current_y = this_arc[-1][1]
+                                current_x, current_y = circular_x, circular_y
+
+                                path += this_arc
+                                last_path_aperture = current_aperture
+                                valid = True
+                                break
+
+                        if valid:
+                            continue
+                        else:
+                            log.warning("Invalid arc in line %d." % line_num)
+
+                # ## EOF
+                match = self.eof_re.search(gline)
+                if match:
+                    continue
+
+                # ## Line did not match any pattern. Warn user.
+                log.warning("Line ignored (%d): %s" % (line_num, gline))
+
+            if len(path) > 1:
+                # In case that G01 (moving) aperture is rectangular, there is no need to still create
+                # another geo since we already created a shapely box using the start and end coordinates found in
+                # path variable. We do it only for other apertures than 'R' type
+                if self.apertures[last_path_aperture]["type"] == 'R':
+                    pass
+                else:
+                    # EOF, create shapely LineString if something still in path
+                    # ## --- Buffered ---
+
+                    geo_dict = dict()
+                    # this treats the case when we are storing geometry as paths
+                    geo_f = LineString(path)
+                    if not geo_f.is_empty:
+                        follow_buffer.append(geo_f)
+                        geo_dict['follow'] = geo_f
+
+                    # this treats the case when we are storing geometry as solids
+                    width = self.apertures[last_path_aperture]["size"]
+                    geo_s = LineString(path).buffer(width / 1.999, int(self.steps_per_circle / 4))
+                    if not geo_s.is_empty:
+                        if self.app.defaults['gerber_simplification']:
+                            poly_buffer.append(geo_s.simplify(s_tol))
+                        else:
+                            poly_buffer.append(geo_s)
+
+                        if self.is_lpc is True:
+                            geo_dict['clear'] = geo_s
+                        else:
+                            geo_dict['solid'] = geo_s
+
+                    if last_path_aperture not in self.apertures:
+                        self.apertures[last_path_aperture] = dict()
+                    if 'geometry' not in self.apertures[last_path_aperture]:
+                        self.apertures[last_path_aperture]['geometry'] = []
+                    self.apertures[last_path_aperture]['geometry'].append(deepcopy(geo_dict))
+
+            # TODO: make sure to keep track of units changes because right now it seems to happen in a weird way
+            # find out the conversion factor used to convert inside the self.apertures keys: size, width, height
+            file_units = self.gerber_units if self.gerber_units else 'IN'
+            app_units = self.app.defaults['units']
+
+            conversion_factor = 25.4 if file_units == 'IN' else (1 / 25.4) if file_units != app_units else 1
+
+            # --- Apply buffer ---
+            # this treats the case when we are storing geometry as paths
+            self.follow_geometry = follow_buffer
+
+            # this treats the case when we are storing geometry as solids
+
+            if len(poly_buffer) == 0:
+                log.error("Object is not Gerber file or empty. Aborting Object creation.")
+                return 'fail'
+
+            log.warning("Joining %d polygons." % len(poly_buffer))
+            self.app.inform.emit('%s: %d.' % (_("Gerber processing. Joining polygons"), len(poly_buffer)))
+
+            if self.use_buffer_for_union:
+                log.debug("Union by buffer...")
+
+                new_poly = MultiPolygon(poly_buffer)
+                if self.app.defaults["gerber_buffering"] == 'full':
+                    new_poly = new_poly.buffer(0.00000001)
+                    new_poly = new_poly.buffer(-0.00000001)
+                log.warning("Union(buffer) done.")
+            else:
+                log.debug("Union by union()...")
+                new_poly = cascaded_union(poly_buffer)
+                new_poly = new_poly.buffer(0, int(self.steps_per_circle / 4))
+                log.warning("Union done.")
+
+            if current_polarity == 'D':
+                self.app.inform.emit('%s' % _("Gerber processing. Applying Gerber polarity."))
+                if new_poly.is_valid:
+                    self.solid_geometry = self.solid_geometry.union(new_poly)
+                else:
+                    # I do this so whenever the parsed geometry of the file is not valid (intersections) it is still
+                    # loaded. Instead of applying a union I add to a list of polygons.
+                    final_poly = []
+                    try:
+                        for poly in new_poly:
+                            final_poly.append(poly)
+                    except TypeError:
+                        final_poly.append(new_poly)
+
+                    try:
+                        for poly in self.solid_geometry:
+                            final_poly.append(poly)
+                    except TypeError:
+                        final_poly.append(self.solid_geometry)
+
+                    self.solid_geometry = final_poly
+
+                # try:
+                #     self.solid_geometry = self.solid_geometry.union(new_poly)
+                # except Exception as e:
+                #     # in case in the new_poly are some self intersections try to avoid making union with them
+                #     for poly in new_poly:
+                #         try:
+                #             self.solid_geometry = self.solid_geometry.union(poly)
+                #         except:
+                #             pass
+            else:
+                self.solid_geometry = self.solid_geometry.difference(new_poly)
+        except Exception as err:
+            ex_type, ex, tb = sys.exc_info()
+            traceback.print_tb(tb)
+            # print traceback.format_exc()
+
+            log.error("Gerber PARSING FAILED. Line %d: %s" % (line_num, gline))
+
+            loc = '%s #%d %s: %s\n' % (_("Gerber Line"), line_num, _("Gerber Line Content"), gline) + repr(err)
+            self.app.inform.emit('[ERROR] %s\n%s:' %
+                                 (_("Gerber Parser ERROR"), loc))
+
+    @staticmethod
+    def create_flash_geometry(location, aperture, steps_per_circle=None):
+
+        # log.debug('Flashing @%s, Aperture: %s' % (location, aperture))
+
+        if type(location) == list:
+            location = Point(location)
+
+        if aperture['type'] == 'C':  # Circles
+            return location.buffer(aperture['size'] / 2, int(steps_per_circle / 4))
+
+        if aperture['type'] == 'R':  # Rectangles
+            loc = location.coords[0]
+            width = aperture['width']
+            height = aperture['height']
+            minx = loc[0] - width / 2
+            maxx = loc[0] + width / 2
+            miny = loc[1] - height / 2
+            maxy = loc[1] + height / 2
+            return shply_box(minx, miny, maxx, maxy)
+
+        if aperture['type'] == 'O':  # Obround
+            loc = location.coords[0]
+            width = aperture['width']
+            height = aperture['height']
+            if width > height:
+                p1 = Point(loc[0] + 0.5 * (width - height), loc[1])
+                p2 = Point(loc[0] - 0.5 * (width - height), loc[1])
+                c1 = p1.buffer(height * 0.5, int(steps_per_circle / 4))
+                c2 = p2.buffer(height * 0.5, int(steps_per_circle / 4))
+            else:
+                p1 = Point(loc[0], loc[1] + 0.5 * (height - width))
+                p2 = Point(loc[0], loc[1] - 0.5 * (height - width))
+                c1 = p1.buffer(width * 0.5, int(steps_per_circle / 4))
+                c2 = p2.buffer(width * 0.5, int(steps_per_circle / 4))
+            return cascaded_union([c1, c2]).convex_hull
+
+        if aperture['type'] == 'P':  # Regular polygon
+            loc = location.coords[0]
+            diam = aperture['diam']
+            n_vertices = aperture['nVertices']
+            points = []
+            for i in range(0, n_vertices):
+                x = loc[0] + 0.5 * diam * (cos(2 * pi * i / n_vertices))
+                y = loc[1] + 0.5 * diam * (sin(2 * pi * i / n_vertices))
+                points.append((x, y))
+            ply = Polygon(points)
+            if 'rotation' in aperture:
+                ply = affinity.rotate(ply, aperture['rotation'])
+            return ply
+
+        if aperture['type'] == 'AM':  # Aperture Macro
+            loc = location.coords[0]
+            flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
+            if flash_geo.is_empty:
+                log.warning("Empty geometry for Aperture Macro: %s" % str(aperture['macro'].name))
+            return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
+
+        log.warning("Unknown aperture type: %s" % aperture['type'])
+        return None
+
+    def create_geometry(self):
+        """
+        Geometry from a Gerber file is made up entirely of polygons.
+        Every stroke (linear or circular) has an aperture which gives
+        it thickness. Additionally, aperture strokes have non-zero area,
+        and regions naturally do as well.
+
+        :rtype : None
+        :return: None
+        """
+        pass
+        # self.buffer_paths()
+        #
+        # self.fix_regions()
+        #
+        # self.do_flashes()
+        #
+        # self.solid_geometry = cascaded_union(self.buffered_paths +
+        #                                      [poly['polygon'] for poly in self.regions] +
+        #                                      self.flash_geometry)
+
+    def get_bounding_box(self, margin=0.0, rounded=False):
+        """
+        Creates and returns a rectangular polygon bounding at a distance of
+        margin from the object's ``solid_geometry``. If margin > 0, the polygon
+        can optionally have rounded corners of radius equal to margin.
+
+        :param margin: Distance to enlarge the rectangular bounding
+         box in both positive and negative, x and y axes.
+        :type margin: float
+        :param rounded: Wether or not to have rounded corners.
+        :type rounded: bool
+        :return: The bounding box.
+        :rtype: Shapely.Polygon
+        """
+
+        bbox = self.solid_geometry.envelope.buffer(margin)
+        if not rounded:
+            bbox = bbox.envelope
+        return bbox
+
+    def bounds(self):
+        """
+        Returns coordinates of rectangular bounds
+        of Gerber geometry: (xmin, ymin, xmax, ymax).
+        """
+        # fixed issue of getting bounds only for one level lists of objects
+        # now it can get bounds for nested lists of objects
+
+        log.debug("camlib.Gerber.bounds()")
+
+        if self.solid_geometry is None:
+            log.debug("solid_geometry is None")
+            return 0, 0, 0, 0
+
+        def bounds_rec(obj):
+            if type(obj) is list and type(obj) is not MultiPolygon:
+                minx = Inf
+                miny = Inf
+                maxx = -Inf
+                maxy = -Inf
+
+                for k in obj:
+                    if type(k) is dict:
+                        for key in k:
+                            minx_, miny_, maxx_, maxy_ = bounds_rec(k[key])
+                            minx = min(minx, minx_)
+                            miny = min(miny, miny_)
+                            maxx = max(maxx, maxx_)
+                            maxy = max(maxy, maxy_)
+                    else:
+                        if not k.is_empty:
+                            try:
+                                minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                            except Exception as e:
+                                log.debug("camlib.Gerber.bounds() --> %s" % str(e))
+                                return
+
+                            minx = min(minx, minx_)
+                            miny = min(miny, miny_)
+                            maxx = max(maxx, maxx_)
+                            maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                return obj.bounds
+
+        bounds_coords = bounds_rec(self.solid_geometry)
+        return bounds_coords
+
+    def scale(self, xfactor, yfactor=None, point=None):
+        """
+        Scales the objects' geometry on the XY plane by a given factor.
+        These are:
+
+        * ``buffered_paths``
+        * ``flash_geometry``
+        * ``solid_geometry``
+        * ``regions``
+
+        NOTE:
+        Does not modify the data used to create these elements. If these
+        are recreated, the scaling will be lost. This behavior was modified
+        because of the complexity reached in this class.
+
+        :param xfactor: Number by which to scale on X axis.
+        :type xfactor: float
+        :param yfactor: Number by which to scale on Y axis.
+        :type yfactor: float
+        :rtype : None
+        """
+        log.debug("camlib.Gerber.scale()")
+
+        try:
+            xfactor = float(xfactor)
+        except:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("Scale factor has to be a number: integer or float."))
+            return
+
+        if yfactor is None:
+            yfactor = xfactor
+        else:
+            try:
+                yfactor = float(yfactor)
+            except:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("Scale factor has to be a number: integer or float."))
+                return
+
+        if point is None:
+            px = 0
+            py = 0
+        else:
+            px, py = point
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        def scale_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(scale_geom(g))
+                return new_obj
+            else:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.scale(obj, xfactor, yfactor, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = scale_geom(self.solid_geometry)
+        self.follow_geometry = scale_geom(self.follow_geometry)
+
+        # we need to scale the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = scale_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = scale_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = scale_geom(geo_el['clear'])
+
+        except Exception as e:
+            log.debug('camlib.Gerber.scale() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Scale done."))
+        self.app.proc_container.new_text = ''
+
+        # ## solid_geometry ???
+        #  It's a cascaded union of objects.
+        # self.solid_geometry = affinity.scale(self.solid_geometry, factor,
+        #                                      factor, origin=(0, 0))
+
+        # # Now buffered_paths, flash_geometry and solid_geometry
+        # self.create_geometry()
+
+    def offset(self, vect):
+        """
+        Offsets the objects' geometry on the XY plane by a given vector.
+        These are:
+
+        * ``buffered_paths``
+        * ``flash_geometry``
+        * ``solid_geometry``
+        * ``regions``
+
+        NOTE:
+        Does not modify the data used to create these elements. If these
+        are recreated, the scaling will be lost. This behavior was modified
+        because of the complexity reached in this class.
+
+        :param vect: (x, y) offset vector.
+        :type vect: tuple
+        :return: None
+        """
+        log.debug("camlib.Gerber.offset()")
+
+        try:
+            dx, dy = vect
+        except TypeError:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("An (x,y) pair of values are needed. "
+                                   "Probable you entered only one value in the Offset field."))
+            return
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        def offset_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(offset_geom(g))
+                return new_obj
+            else:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.translate(obj, xoff=dx, yoff=dy)
+                except AttributeError:
+                    return obj
+
+        # ## Solid geometry
+        self.solid_geometry = offset_geom(self.solid_geometry)
+        self.follow_geometry = offset_geom(self.follow_geometry)
+
+        # we need to offset the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = offset_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = offset_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = offset_geom(geo_el['clear'])
+
+        except Exception as e:
+            log.debug('camlib.Gerber.offset() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Offset done."))
+        self.app.proc_container.new_text = ''
+
+    def mirror(self, axis, point):
+        """
+        Mirrors the object around a specified axis passing through
+        the given point. What is affected:
+
+        * ``buffered_paths``
+        * ``flash_geometry``
+        * ``solid_geometry``
+        * ``regions``
+
+        NOTE:
+        Does not modify the data used to create these elements. If these
+        are recreated, the scaling will be lost. This behavior was modified
+        because of the complexity reached in this class.
+
+        :param axis: "X" or "Y" indicates around which axis to mirror.
+        :type axis: str
+        :param point: [x, y] point belonging to the mirror axis.
+        :type point: list
+        :return: None
+        """
+        log.debug("camlib.Gerber.mirror()")
+
+        px, py = point
+        xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        def mirror_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(mirror_geom(g))
+                return new_obj
+            else:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 99]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.scale(obj, xscale, yscale, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = mirror_geom(self.solid_geometry)
+        self.follow_geometry = mirror_geom(self.follow_geometry)
+
+        # we need to mirror the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = mirror_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = mirror_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = mirror_geom(geo_el['clear'])
+        except Exception as e:
+            log.debug('camlib.Gerber.mirror() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Mirror done."))
+        self.app.proc_container.new_text = ''
+
+    def skew(self, angle_x, angle_y, point):
+        """
+        Shear/Skew the geometries of an object by angles along x and y dimensions.
+
+        Parameters
+        ----------
+        angle_x, angle_y : float, float
+            The shear angle(s) for the x and y axes respectively. These can be
+            specified in either degrees (default) or radians by setting
+            use_radians=True.
+
+        See shapely manual for more information:
+        http://toblerity.org/shapely/manual.html#affine-transformations
+        """
+        log.debug("camlib.Gerber.skew()")
+
+        px, py = point
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        def skew_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(skew_geom(g))
+                return new_obj
+            else:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.skew(obj, angle_x, angle_y, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = skew_geom(self.solid_geometry)
+        self.follow_geometry = skew_geom(self.follow_geometry)
+
+        # we need to skew the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = skew_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = skew_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = skew_geom(geo_el['clear'])
+        except Exception as e:
+            log.debug('camlib.Gerber.skew() Exception --> %s' % str(e))
+            return 'fail'
+
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Skew done."))
+        self.app.proc_container.new_text = ''
+
+    def rotate(self, angle, point):
+        """
+        Rotate an object by a given angle around given coords (point)
+        :param angle:
+        :param point:
+        :return:
+        """
+        log.debug("camlib.Gerber.rotate()")
+
+        px, py = point
+
+        # variables to display the percentage of work done
+        self.geo_len = 0
+        try:
+            for g in self.solid_geometry:
+                self.geo_len += 1
+        except TypeError:
+            self.geo_len = 1
+
+        self.old_disp_number = 0
+        self.el_count = 0
+
+        def rotate_geom(obj):
+            if type(obj) is list:
+                new_obj = []
+                for g in obj:
+                    new_obj.append(rotate_geom(g))
+                return new_obj
+            else:
+                try:
+                    self.el_count += 1
+                    disp_number = int(np.interp(self.el_count, [0, self.geo_len], [0, 100]))
+                    if self.old_disp_number < disp_number <= 100:
+                        self.app.proc_container.update_view_text(' %d%%' % disp_number)
+                        self.old_disp_number = disp_number
+
+                    return affinity.rotate(obj, angle, origin=(px, py))
+                except AttributeError:
+                    return obj
+
+        self.solid_geometry = rotate_geom(self.solid_geometry)
+        self.follow_geometry = rotate_geom(self.follow_geometry)
+
+        # we need to rotate the geometry stored in the Gerber apertures, too
+        try:
+            for apid in self.apertures:
+                if 'geometry' in self.apertures[apid]:
+                    for geo_el in self.apertures[apid]['geometry']:
+                        if 'solid' in geo_el:
+                            geo_el['solid'] = rotate_geom(geo_el['solid'])
+                        if 'follow' in geo_el:
+                            geo_el['follow'] = rotate_geom(geo_el['follow'])
+                        if 'clear' in geo_el:
+                            geo_el['clear'] = rotate_geom(geo_el['clear'])
+        except Exception as e:
+            log.debug('camlib.Gerber.rotate() Exception --> %s' % str(e))
+            return 'fail'
+        self.app.inform.emit('[success] %s' %
+                             _("Gerber Rotate done."))
+        self.app.proc_container.new_text = ''