瀏覽代碼

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

Marius Stanciu 6 年之前
父節點
當前提交
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 FlatCAMCommon import LoudDict
 from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 from camlib import *
 from camlib import *
+from flatcamParsers.ParseExcellon import Excellon
+from flatcamParsers.ParseGerber import Gerber
 
 
 import itertools
 import itertools
 import tkinter as tk
 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
 - 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
 - added display of the results for the Rules Check Tool in a formatted way
 - made the Rules Check Tool document window Read Only
 - made the Rules Check Tool document window Read Only
+- made FlatCAMExcellon and FlatCAMGerber into their own files in the flatcamParser folder
 
 
 5.10.2019
 5.10.2019
 
 

+ 4 - 3404
camlib.py

@@ -25,7 +25,6 @@ from rtree import index as rtindex
 from lxml import etree as ET
 from lxml import etree as ET
 
 
 # See: http://toblerity.org/shapely/manual.html
 # See: http://toblerity.org/shapely/manual.html
-
 from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString
 from shapely.geometry import Polygon, LineString, Point, LinearRing, MultiLineString
 from shapely.geometry import MultiPoint, MultiPolygon
 from shapely.geometry import MultiPoint, MultiPolygon
 from shapely.geometry import box as shply_box
 from shapely.geometry import box as shply_box
@@ -54,15 +53,16 @@ import ezdxf
 from flatcamParsers.ParseSVG import *
 from flatcamParsers.ParseSVG import *
 from flatcamParsers.ParseDXF 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 logging
 import FlatCAMApp
 import FlatCAMApp
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 import builtins
 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')
 fcTranslate.apply_language('strings')
 
 
@@ -2075,3406 +2075,6 @@ class ApertureMacro:
         return self.geometry
         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):
 class AttrDict(dict):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super(AttrDict, self).__init__(*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 camlib import *
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, SpinBoxDelegate
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
 from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
+from flatcamParsers.ParseExcellon import Excellon
 
 
 from copy import copy, deepcopy
 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, \
 from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, LengthEntry, RadioSet, \
     SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
     SpinBoxDelegate, EvalEntry, EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
 from FlatCAMObj import FlatCAMGerber
 from FlatCAMObj import FlatCAMGerber
+from flatcamParsers.ParseGerber import Gerber
 from FlatCAMTool import FlatCAMTool
 from FlatCAMTool import FlatCAMTool
 
 
 from numpy.linalg import norm as numpy_norm
 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 = ''