Browse Source

- updated the Film Tool to allow exporting PDF and PNG file (besides the SVG file)

Marius Stanciu 6 years ago
parent
commit
f1af9d7999
6 changed files with 490 additions and 343 deletions
  1. 4 287
      FlatCAMApp.py
  2. 4 0
      README.md
  3. 21 0
      flatcamGUI/PreferencesUI.py
  4. 458 55
      flatcamTools/ToolFilm.py
  5. 2 0
      requirements.txt
  6. 1 1
      setup_ubuntu.sh

+ 4 - 287
FlatCAMApp.py

@@ -557,7 +557,7 @@ class App(QtCore.QObject):
             "gerber_editor_newdim": "0.5, 0.5",
             "gerber_editor_newdim": "0.5, 0.5",
             "gerber_editor_array_size": 5,
             "gerber_editor_array_size": 5,
             "gerber_editor_lin_axis": 'X',
             "gerber_editor_lin_axis": 'X',
-            "gerber_editor_lin_pitch": 1,
+            "gerber_editor_lin_pitch": 0.1,
             "gerber_editor_lin_angle": 0.0,
             "gerber_editor_lin_angle": 0.0,
             "gerber_editor_circ_dir": 'CW',
             "gerber_editor_circ_dir": 'CW',
             "gerber_editor_circ_angle": 0.0,
             "gerber_editor_circ_angle": 0.0,
@@ -765,6 +765,7 @@ class App(QtCore.QObject):
             "tools_film_skew_ref_radio": 'bottomleft',
             "tools_film_skew_ref_radio": 'bottomleft',
             "tools_film_mirror_cb": False,
             "tools_film_mirror_cb": False,
             "tools_film_mirror_axis_radio": 'none',
             "tools_film_mirror_axis_radio": 'none',
+            "tools_film_file_type_radio": 'svg',
 
 
             # Panel Tool
             # Panel Tool
             "tools_panelize_spacing_columns": 0,
             "tools_panelize_spacing_columns": 0,
@@ -1322,6 +1323,7 @@ class App(QtCore.QObject):
             "tools_film_skew_ref_radio": self.ui.tools_defaults_form.tools_film_group.film_skew_reference,
             "tools_film_skew_ref_radio": self.ui.tools_defaults_form.tools_film_group.film_skew_reference,
             "tools_film_mirror_cb": self.ui.tools_defaults_form.tools_film_group.film_mirror_cb,
             "tools_film_mirror_cb": self.ui.tools_defaults_form.tools_film_group.film_mirror_cb,
             "tools_film_mirror_axis_radio": self.ui.tools_defaults_form.tools_film_group.film_mirror_axis,
             "tools_film_mirror_axis_radio": self.ui.tools_defaults_form.tools_film_group.film_mirror_axis,
+            "tools_film_file_type_radio": self.ui.tools_defaults_form.tools_film_group.file_type_radio,
 
 
             # Panelize Tool
             # Panelize Tool
             "tools_panelize_spacing_columns": self.ui.tools_defaults_form.tools_panelize_group.pspacing_columns,
             "tools_panelize_spacing_columns": self.ui.tools_defaults_form.tools_panelize_group.pspacing_columns,
@@ -5560,7 +5562,7 @@ class App(QtCore.QObject):
         if self.toggle_units_ignore:
         if self.toggle_units_ignore:
             return
             return
 
 
-        new_units = self.defaults['units'].upper()
+        new_units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
 
 
         # If option is the same, then ignore
         # If option is the same, then ignore
         if new_units == self.defaults["units"].upper():
         if new_units == self.defaults["units"].upper():
@@ -10059,291 +10061,6 @@ class App(QtCore.QObject):
             self.inform.emit('[success] %s: %s' %
             self.inform.emit('[success] %s: %s' %
                              (_("SVG file exported to"), filename))
                              (_("SVG file exported to"), filename))
 
 
-    def export_svg_negative(self, obj_name, box_name, filename, boundary,
-                            scale_stroke_factor=0.00,
-                            scale_factor_x=None, scale_factor_y=None,
-                            skew_factor_x=None, skew_factor_y=None, skew_reference='center',
-                            mirror=None,
-                            use_thread=True):
-        """
-        Exports a Geometry Object to an SVG file in negative.
-
-        :param obj_name: the name of the FlatCAM object to be saved as SVG
-        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
-        :param filename: Path to the SVG file to save to.
-        :param boundary: thickness of a black border to surround all the features
-        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
-        :param scale_factor_x: factor to scale the svg geometry on the X axis
-        :param scale_factor_y: factor to scale the svg geometry on the Y axis
-        :param skew_factor_x: factor to skew the svg geometry on the X axis
-        :param skew_factor_y: factor to skew the svg geometry on the Y axis
-        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
-        those are the 4 points of the bounding box of the geometry to be skewed.
-        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
-        :param use_thread: if to be run in a separate thread; boolean
-        :return:
-        """
-        self.report_usage("export_negative()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("export_svg() negative")
-
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except Exception:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
-
-        try:
-            box = self.collection.get_by_name(str(box_name))
-        except Exception:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % box_name
-
-        if box is None:
-            self.inform.emit('[WARNING_NOTCL] %s: %s' %
-                             (_("No object Box. Using instead"), obj))
-            box = obj
-
-        def make_negative_film():
-            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
-                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                          mirror=mirror
-                                          )
-
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
-
-            uom = obj.units.lower()
-
-            # Convert everything to strings for use in the xml doc
-            svgwidth = str(size[0] + (2 * boundary))
-            svgheight = str(size[1] + (2 * boundary))
-            minx = str(bounds[0] - boundary)
-            miny = str(bounds[1] + boundary + size[1])
-            miny_rect = str(bounds[1] - boundary)
-
-            # Add a SVG Header and footer to the svg output from shapely
-            # The transform flips the Y Axis so that everything renders
-            # properly within svg apps such as inkscape
-            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
-                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
-            svg_header += 'width="' + svgwidth + uom + '" '
-            svg_header += 'height="' + svgheight + uom + '" '
-            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
-            svg_header += '>'
-            svg_header += '<g transform="scale(1,-1)">'
-            svg_footer = '</g> </svg>'
-
-            # Change the attributes of the exported SVG
-            # We don't need stroke-width - wrong, we do when we have lines with certain width
-            # We set opacity to maximum
-            # We set the color to WHITE
-            root = ET.fromstring(exported_svg)
-            for child in root:
-                child.set('fill', '#FFFFFF')
-                child.set('opacity', '1.0')
-                child.set('stroke', '#FFFFFF')
-
-            # first_svg_elem = 'rect x="' + minx + '" ' + 'y="' + miny_rect + '" '
-            # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
-            # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
-
-            first_svg_elem_tag = 'rect'
-            first_svg_elem_attribs = {
-                'x': minx,
-                'y': miny_rect,
-                'width': svgwidth,
-                'height': svgheight,
-                'id': 'neg_rect',
-                'style': 'fill:#000000;opacity:1.0;stroke-width:0.0'
-            }
-
-            root.insert(0, ET.Element(first_svg_elem_tag, first_svg_elem_attribs))
-            exported_svg = ET.tostring(root)
-
-            svg_elem = svg_header + str(exported_svg) + svg_footer
-
-            # Parse the xml through a xml parser just to add line feeds
-            # and to make it look more pretty for the output
-            doc = parse_xml_string(svg_elem)
-            try:
-                with open(filename, 'w') as fp:
-                    fp.write(doc.toprettyxml())
-            except PermissionError:
-                self.inform.emit('[WARNING] %s' %
-                                 _("Permission denied, saving not possible.\n"
-                                   "Most likely another app is holding the file open and not accessible."))
-                return 'fail'
-
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("SVG", filename)
-            self.file_saved.emit("SVG", filename)
-            self.inform.emit('[success] %s: %s' %
-                             (_("SVG file exported to"), filename))
-
-        if use_thread is True:
-            proc = self.proc_container.new(_("Generating Film ... Please wait."))
-
-            def job_thread_film(app_obj):
-                try:
-                    make_negative_film()
-                except Exception:
-                    proc.done()
-                    return
-                proc.done()
-
-            self.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
-        else:
-            make_negative_film()
-
-    def export_svg_positive(self, obj_name, box_name, filename,
-                            scale_stroke_factor=0.00,
-                            scale_factor_x=None, scale_factor_y=None,
-                            skew_factor_x=None, skew_factor_y=None, skew_reference='center',
-                            mirror=None,
-                            use_thread=True):
-        """
-        Exports a Geometry Object to an SVG file in positive black.
-
-        :param obj_name: the name of the FlatCAM object to be saved as SVG
-        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
-        :param filename: Path to the SVG file to save to.
-        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
-        :param scale_factor_x: factor to scale the svg geometry on the X axis
-        :param scale_factor_y: factor to scale the svg geometry on the Y axis
-        :param skew_factor_x: factor to skew the svg geometry on the X axis
-        :param skew_factor_y: factor to skew the svg geometry on the Y axis
-        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
-        those are the 4 points of the bounding box of the geometry to be skewed.
-        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
-
-        :param use_thread: if to be run in a separate thread; boolean
-        :return:
-        """
-        self.report_usage("export_svg_positive()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("export_svg() black")
-
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except Exception:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
-
-        try:
-            box = self.collection.get_by_name(str(box_name))
-        except Exception:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % box_name
-
-        if box is None:
-            self.inform.emit('[WARNING_NOTCL] %s: %s' %
-                             (_("No object Box. Using instead"), obj))
-            box = obj
-
-        def make_positive_film():
-            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
-                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                          mirror=mirror
-                                          )
-
-            self.progress.emit(40)
-
-            # Change the attributes of the exported SVG
-            # We don't need stroke-width
-            # We set opacity to maximum
-            # We set the colour to WHITE
-            root = ET.fromstring(exported_svg)
-            for child in root:
-                child.set('fill', str(self.defaults['tools_film_color']))
-                child.set('opacity', '1.0')
-                child.set('stroke', str(self.defaults['tools_film_color']))
-
-            exported_svg = ET.tostring(root)
-
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
-
-            # This contain the measure units
-            uom = obj.units.lower()
-
-            # Define a boundary around SVG of about 1.0mm (~39mils)
-            if uom in "mm":
-                boundary = 1.0
-            else:
-                boundary = 0.0393701
-
-            self.progress.emit(80)
-
-            # Convert everything to strings for use in the xml doc
-            svgwidth = str(size[0] + (2 * boundary))
-            svgheight = str(size[1] + (2 * boundary))
-            minx = str(bounds[0] - boundary)
-            miny = str(bounds[1] + boundary + size[1])
-
-            self.log.debug(minx)
-            self.log.debug(miny)
-
-            # Add a SVG Header and footer to the svg output from shapely
-            # The transform flips the Y Axis so that everything renders
-            # properly within svg apps such as inkscape
-            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
-                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
-            svg_header += 'width="' + svgwidth + uom + '" '
-            svg_header += 'height="' + svgheight + uom + '" '
-            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
-            svg_header += '>'
-            svg_header += '<g transform="scale(1,-1)">'
-            svg_footer = '</g> </svg>'
-
-            svg_elem = str(svg_header) + str(exported_svg) + str(svg_footer)
-
-            self.progress.emit(90)
-
-            # Parse the xml through a xml parser just to add line feeds
-            # and to make it look more pretty for the output
-            doc = parse_xml_string(svg_elem)
-            try:
-                with open(filename, 'w') as fp:
-                    fp.write(doc.toprettyxml())
-            except PermissionError:
-                self.inform.emit('[WARNING] %s' %
-                                 _("Permission denied, saving not possible.\n"
-                                   "Most likely another app is holding the file open and not accessible."))
-                return 'fail'
-
-            self.progress.emit(100)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("SVG", filename)
-            self.file_saved.emit("SVG", filename)
-            self.inform.emit('[success] %s: %s' %
-                             (_("SVG file exported to"), filename))
-
-        if use_thread is True:
-            proc = self.proc_container.new(_("Generating Film ... Please wait."))
-
-            def job_thread_film(app_obj):
-                try:
-                    make_positive_film()
-                except Exception:
-                    proc.done()
-                    return
-                proc.done()
-
-            self.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
-        else:
-            make_positive_film()
-
     def save_source_file(self, obj_name, filename, use_thread=True):
     def save_source_file(self, obj_name, filename, use_thread=True):
         """
         """
         Exports a FlatCAM Object to an Gerber/Excellon file.
         Exports a FlatCAM Object to an Gerber/Excellon file.

+ 4 - 0
README.md

@@ -9,6 +9,10 @@ CAD program, and create G-Code for Isolation routing.
 
 
 =================================================
 =================================================
 
 
+26.11.2019
+
+- updated the Film Tool to allow exporting PDF and PNG file (besides the SVG file)
+
 25.11.2019
 25.11.2019
 
 
 - In Gerber isolation changed the UI
 - In Gerber isolation changed the UI

+ 21 - 0
flatcamGUI/PreferencesUI.py

@@ -4634,6 +4634,27 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.film_mirror_axis_label, 13, 0)
         grid0.addWidget(self.film_mirror_axis_label, 13, 0)
         grid0.addWidget(self.film_mirror_axis, 13, 1)
         grid0.addWidget(self.film_mirror_axis, 13, 1)
 
 
+        separator_line3 = QtWidgets.QFrame()
+        separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line3, 14, 0, 1, 2)
+
+        self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'},
+                                         {'label': _('PNG'), 'value': 'png'},
+                                         {'label': _('PDF'), 'value': 'pdf'}
+                                         ], stretch=False)
+
+        self.file_type_label = QtWidgets.QLabel(_("Film Type:"))
+        self.file_type_label.setToolTip(
+            _("The file type of the saved film. Can be:\n"
+              "- 'SVG' -> open-source vectorial format\n"
+              "- 'PNG' -> raster image\n"
+              "- 'PDF' -> portable document format")
+        )
+        grid0.addWidget(self.file_type_label, 15, 0)
+        grid0.addWidget(self.file_type_radio, 15, 1)
+
+
         self.layout.addStretch()
         self.layout.addStretch()
 
 
 
 

+ 458 - 55
flatcamTools/ToolFilm.py

@@ -15,6 +15,15 @@ from copy import deepcopy
 import logging
 import logging
 from shapely.geometry import Polygon, MultiPolygon, Point
 from shapely.geometry import Polygon, MultiPolygon, Point
 
 
+from reportlab.graphics import renderPDF, renderPM
+from reportlab.pdfgen import canvas
+from reportlab.lib.pagesizes import letter, A0, A1, A2, A3, A4, A5
+
+from svglib.svglib import svg2rlg
+from xml.dom.minidom import parseString as parse_xml_string
+from lxml import etree as ET
+from io import StringIO
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 import builtins
 import builtins
@@ -168,6 +177,12 @@ class Film(FlatCAMTool):
 
 
         self.ois_scale = OptionalInputSection(self.film_scale_cb, [self.film_scalex_label, self.film_scalex_entry,
         self.ois_scale = OptionalInputSection(self.film_scale_cb, [self.film_scalex_label, self.film_scalex_entry,
                                                                    self.film_scaley_label,  self.film_scaley_entry])
                                                                    self.film_scaley_label,  self.film_scaley_entry])
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
+
         # Skew Geometry
         # Skew Geometry
         self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
         self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
         self.film_skew_cb.setToolTip(
         self.film_skew_cb.setToolTip(
@@ -179,7 +194,7 @@ class Film(FlatCAMTool):
             QCheckBox {font-weight: bold; color: black}
             QCheckBox {font-weight: bold; color: black}
             """
             """
         )
         )
-        grid0.addWidget(self.film_skew_cb, 9, 0, 1, 2)
+        grid0.addWidget(self.film_skew_cb, 10, 0, 1, 2)
 
 
         self.film_skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
         self.film_skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
         self.film_skewx_entry = FCDoubleSpinner()
         self.film_skewx_entry = FCDoubleSpinner()
@@ -187,8 +202,8 @@ class Film(FlatCAMTool):
         self.film_skewx_entry.set_precision(self.decimals)
         self.film_skewx_entry.set_precision(self.decimals)
         self.film_skewx_entry.setSingleStep(0.01)
         self.film_skewx_entry.setSingleStep(0.01)
 
 
-        grid0.addWidget(self.film_skewx_label, 10, 0)
-        grid0.addWidget(self.film_skewx_entry, 10, 1)
+        grid0.addWidget(self.film_skewx_label, 11, 0)
+        grid0.addWidget(self.film_skewx_entry, 11, 1)
 
 
         self.film_skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
         self.film_skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
         self.film_skewy_entry = FCDoubleSpinner()
         self.film_skewy_entry = FCDoubleSpinner()
@@ -196,8 +211,8 @@ class Film(FlatCAMTool):
         self.film_skewy_entry.set_precision(self.decimals)
         self.film_skewy_entry.set_precision(self.decimals)
         self.film_skewy_entry.setSingleStep(0.01)
         self.film_skewy_entry.setSingleStep(0.01)
 
 
-        grid0.addWidget(self.film_skewy_label, 11, 0)
-        grid0.addWidget(self.film_skewy_entry, 11, 1)
+        grid0.addWidget(self.film_skewy_label, 12, 0)
+        grid0.addWidget(self.film_skewy_entry, 12, 1)
 
 
         self.film_skew_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
         self.film_skew_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
         self.film_skew_ref_label.setToolTip(
         self.film_skew_ref_label.setToolTip(
@@ -211,12 +226,18 @@ class Film(FlatCAMTool):
                                             orientation='vertical',
                                             orientation='vertical',
                                             stretch=False)
                                             stretch=False)
 
 
-        grid0.addWidget(self.film_skew_ref_label, 12, 0)
-        grid0.addWidget(self.film_skew_reference, 12, 1)
+        grid0.addWidget(self.film_skew_ref_label, 13, 0)
+        grid0.addWidget(self.film_skew_reference, 13, 1)
 
 
         self.ois_skew = OptionalInputSection(self.film_skew_cb, [self.film_skewx_label, self.film_skewx_entry,
         self.ois_skew = OptionalInputSection(self.film_skew_cb, [self.film_skewx_label, self.film_skewx_entry,
                                                                  self.film_skewy_label,  self.film_skewy_entry,
                                                                  self.film_skewy_label,  self.film_skewy_entry,
                                                                  self.film_skew_reference])
                                                                  self.film_skew_reference])
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line1, 14, 0, 1, 2)
+
         # Mirror Geometry
         # Mirror Geometry
         self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
         self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
         self.film_mirror_cb.setToolTip(
         self.film_mirror_cb.setToolTip(
@@ -227,7 +248,7 @@ class Film(FlatCAMTool):
             QCheckBox {font-weight: bold; color: black}
             QCheckBox {font-weight: bold; color: black}
             """
             """
         )
         )
-        grid0.addWidget(self.film_mirror_cb, 13, 0, 1, 2)
+        grid0.addWidget(self.film_mirror_cb, 15, 0, 1, 2)
 
 
         self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
         self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
                                           {'label': _('X'), 'value': 'x'},
                                           {'label': _('X'), 'value': 'x'},
@@ -236,13 +257,16 @@ class Film(FlatCAMTool):
                                          stretch=False)
                                          stretch=False)
         self.film_mirror_axis_label = QtWidgets.QLabel('%s:' % _("Mirror axis"))
         self.film_mirror_axis_label = QtWidgets.QLabel('%s:' % _("Mirror axis"))
 
 
-        grid0.addWidget(self.film_mirror_axis_label, 14, 0)
-        grid0.addWidget(self.film_mirror_axis, 14, 1)
+        grid0.addWidget(self.film_mirror_axis_label, 16, 0)
+        grid0.addWidget(self.film_mirror_axis, 16, 1)
 
 
         self.ois_mirror = OptionalInputSection(self.film_mirror_cb,
         self.ois_mirror = OptionalInputSection(self.film_mirror_cb,
                                                [self.film_mirror_axis_label, self.film_mirror_axis])
                                                [self.film_mirror_axis_label, self.film_mirror_axis])
 
 
-        grid0.addWidget(QtWidgets.QLabel(''), 15, 0)
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line2, 17, 0, 1, 2)
 
 
         # Scale Stroke size
         # Scale Stroke size
         self.film_scale_stroke_entry = FCDoubleSpinner()
         self.film_scale_stroke_entry = FCDoubleSpinner()
@@ -256,10 +280,10 @@ class Film(FlatCAMTool):
               "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
               "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
               "therefore the fine features may be more affected by this parameter.")
               "therefore the fine features may be more affected by this parameter.")
         )
         )
-        grid0.addWidget(self.film_scale_stroke_label, 16, 0)
-        grid0.addWidget(self.film_scale_stroke_entry, 16, 1)
+        grid0.addWidget(self.film_scale_stroke_label, 18, 0)
+        grid0.addWidget(self.film_scale_stroke_entry, 18, 1)
 
 
-        grid0.addWidget(QtWidgets.QLabel(''), 17, 0)
+        grid0.addWidget(QtWidgets.QLabel(''), 19, 0)
 
 
         # Film Type
         # Film Type
         self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
         self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
@@ -274,8 +298,8 @@ class Film(FlatCAMTool):
               "with white on a black canvas.\n"
               "with white on a black canvas.\n"
               "The Film format is SVG.")
               "The Film format is SVG.")
         )
         )
-        grid0.addWidget(self.film_type_label, 18, 0)
-        grid0.addWidget(self.film_type, 18, 1)
+        grid0.addWidget(self.film_type_label, 20, 0)
+        grid0.addWidget(self.film_type, 20, 1)
 
 
         # Boundary for negative film generation
         # Boundary for negative film generation
         self.boundary_entry = FCDoubleSpinner()
         self.boundary_entry = FCDoubleSpinner()
@@ -294,8 +318,8 @@ class Film(FlatCAMTool):
               "white color like the rest and which may confound with the\n"
               "white color like the rest and which may confound with the\n"
               "surroundings if not for this border.")
               "surroundings if not for this border.")
         )
         )
-        grid0.addWidget(self.boundary_label, 19, 0)
-        grid0.addWidget(self.boundary_entry, 19, 1)
+        grid0.addWidget(self.boundary_label, 21, 0)
+        grid0.addWidget(self.boundary_entry, 21, 1)
 
 
         self.boundary_label.hide()
         self.boundary_label.hide()
         self.boundary_entry.hide()
         self.boundary_entry.hide()
@@ -305,7 +329,7 @@ class Film(FlatCAMTool):
         self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
         self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
                                    "the generated film is positive. This is done to help drilling,\n"
                                    "the generated film is positive. This is done to help drilling,\n"
                                    "when done manually."))
                                    "when done manually."))
-        grid0.addWidget(self.punch_cb, 20, 0, 1, 2)
+        grid0.addWidget(self.punch_cb, 22, 0, 1, 2)
 
 
         # this way I can hide/show the frame
         # this way I can hide/show the frame
         self.punch_frame = QtWidgets.QFrame()
         self.punch_frame = QtWidgets.QFrame()
@@ -359,10 +383,32 @@ class Film(FlatCAMTool):
         self.punch_size_label.hide()
         self.punch_size_label.hide()
         self.punch_size_spinner.hide()
         self.punch_size_spinner.hide()
 
 
-        # Buttons
-        hlay = QtWidgets.QHBoxLayout()
-        self.layout.addLayout(hlay)
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        separator_line3 = QtWidgets.QFrame()
+        separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line3, 0, 0, 1, 2)
+
+        self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'},
+                                         {'label': _('PNG'), 'value': 'png'},
+                                         {'label': _('PDF'), 'value': 'pdf'}
+                                         ], stretch=False)
+
+        self.file_type_label = QtWidgets.QLabel(_("Film Type:"))
+        self.file_type_label.setToolTip(
+            _("The file type of the saved film. Can be:\n"
+              "- 'SVG' -> open-source vectorial format\n"
+              "- 'PNG' -> raster image\n"
+              "- 'PDF' -> portable document format")
+        )
+        grid1.addWidget(self.file_type_label, 1, 0)
+        grid1.addWidget(self.file_type_radio, 1, 1)
 
 
+        # Buttons
         self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
         self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
         self.film_object_button.setToolTip(
         self.film_object_button.setToolTip(
             _("Create a Film for the selected object, within\n"
             _("Create a Film for the selected object, within\n"
@@ -370,10 +416,12 @@ class Film(FlatCAMTool):
               "FlatCAM object, but directly save it in SVG format\n"
               "FlatCAM object, but directly save it in SVG format\n"
               "which can be opened with Inkscape.")
               "which can be opened with Inkscape.")
         )
         )
-        hlay.addWidget(self.film_object_button)
+        grid1.addWidget(self.film_object_button, 2, 0, 1, 2)
 
 
         self.layout.addStretch()
         self.layout.addStretch()
 
 
+        self.units = self.app.defaults['units']
+
         # ## Signals
         # ## Signals
         self.film_object_button.clicked.connect(self.on_film_creation)
         self.film_object_button.clicked.connect(self.on_film_creation)
         self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
         self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
@@ -449,6 +497,7 @@ class Film(FlatCAMTool):
         self.film_skew_reference.set_value(self.app.defaults["tools_film_skew_ref_radio"])
         self.film_skew_reference.set_value(self.app.defaults["tools_film_skew_ref_radio"])
         self.film_mirror_cb.set_value(self.app.defaults["tools_film_mirror_cb"])
         self.film_mirror_cb.set_value(self.app.defaults["tools_film_mirror_cb"])
         self.film_mirror_axis.set_value(self.app.defaults["tools_film_mirror_axis_radio"])
         self.film_mirror_axis.set_value(self.app.defaults["tools_film_mirror_axis_radio"])
+        self.file_type_radio.set_value(self.app.defaults["tools_film_file_type_radio"])
 
 
     def on_film_type(self, val):
     def on_film_type(self, val):
         type_of_film = val
         type_of_film = val
@@ -485,21 +534,21 @@ class Film(FlatCAMTool):
 
 
         try:
         try:
             name = self.tf_object_combo.currentText()
             name = self.tf_object_combo.currentText()
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("No FlatCAM object selected. Load an object for Film and retry."))
                                  _("No FlatCAM object selected. Load an object for Film and retry."))
             return
             return
 
 
         try:
         try:
             boxname = self.tf_box_combo.currentText()
             boxname = self.tf_box_combo.currentText()
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
             return
             return
 
 
         scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
         scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
-
         source = self.source_punch.get_value()
         source = self.source_punch.get_value()
+        file_type = self.file_type_radio.get_value()
 
 
         # #################################################################
         # #################################################################
         # ################ STARTING THE JOB ###############################
         # ################ STARTING THE JOB ###############################
@@ -510,13 +559,13 @@ class Film(FlatCAMTool):
         if self.film_type.get_value() == "pos":
         if self.film_type.get_value() == "pos":
 
 
             if self.punch_cb.get_value() is False:
             if self.punch_cb.get_value() is False:
-                self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width)
+                self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
             else:
             else:
-                self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width)
+                self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width, ftype=file_type)
         else:
         else:
-            self.generate_negative_film(name, boxname, factor=scale_stroke_width)
+            self.generate_negative_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
 
 
-    def generate_positive_normal_film(self, name, boxname, factor):
+    def generate_positive_normal_film(self, name, boxname, factor, ftype='svg'):
         log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
         log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
 
 
         scale_factor_x = None
         scale_factor_x = None
@@ -541,29 +590,40 @@ class Film(FlatCAMTool):
         if self.film_mirror_cb.get_value():
         if self.film_mirror_cb.get_value():
             if self.film_mirror_axis.get_value() != 'none':
             if self.film_mirror_axis.get_value() != 'none':
                 mirror = self.film_mirror_axis.get_value()
                 mirror = self.film_mirror_axis.get_value()
+
+        if ftype == 'svg':
+            filter_ext = "SVG Files (*.SVG);;"\
+                         "All Files (*.*)"
+        elif ftype == 'png':
+            filter_ext = "PNG Files (*.PNG);;" \
+                         "All Files (*.*)"
+        else:
+            filter_ext = "PDF Files (*.PDF);;" \
+                         "All Files (*.*)"
+
         try:
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export SVG positive"),
+                caption=_("Export positive film"),
                 directory=self.app.get_last_save_folder() + '/' + name,
                 directory=self.app.get_last_save_folder() + '/' + name,
-                filter="*.svg")
+                filter=filter_ext)
         except TypeError:
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG positive"))
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export positive film"))
 
 
         filename = str(filename)
         filename = str(filename)
 
 
         if str(filename) == "":
         if str(filename) == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG positive cancelled."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export positive film cancelled."))
             return
             return
         else:
         else:
-            self.app.export_svg_positive(name, boxname, filename,
-                                         scale_stroke_factor=factor,
-                                         scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                         skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                         skew_reference=skew_reference,
-                                         mirror=mirror
-                                         )
+            self.export_positive(name, boxname, filename,
+                                 scale_stroke_factor=factor,
+                                 scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                 skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                 skew_reference=skew_reference,
+                                 mirror=mirror, ftype=ftype
+                                 )
 
 
-    def generate_positive_punched_film(self, name, boxname, source, factor):
+    def generate_positive_punched_film(self, name, boxname, source, factor, ftype='svg'):
 
 
         film_obj = self.app.collection.get_by_name(name)
         film_obj = self.app.collection.get_by_name(name)
 
 
@@ -572,7 +632,7 @@ class Film(FlatCAMTool):
 
 
             try:
             try:
                 exc_name = self.exc_combo.currentText()
                 exc_name = self.exc_combo.currentText()
-            except Exception as e:
+            except Exception:
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("No Excellon object selected. Load an object for punching reference and retry."))
                                      _("No Excellon object selected. Load an object for punching reference and retry."))
                 return
                 return
@@ -640,7 +700,7 @@ class Film(FlatCAMTool):
 
 
             self.generate_positive_normal_film(outname, boxname, factor=factor)
             self.generate_positive_normal_film(outname, boxname, factor=factor)
 
 
-    def generate_negative_film(self, name, boxname, factor):
+    def generate_negative_film(self, name, boxname, factor, ftype='svg'):
         log.debug("ToolFilm.Film.generate_negative_film() started ...")
         log.debug("ToolFilm.Film.generate_negative_film() started ...")
 
 
         scale_factor_x = None
         scale_factor_x = None
@@ -671,27 +731,370 @@ class Film(FlatCAMTool):
         if border is None:
         if border is None:
             border = 0
             border = 0
 
 
+        if ftype == 'svg':
+            filter_ext = "SVG Files (*.SVG);;"\
+                         "All Files (*.*)"
+        elif ftype == 'png':
+            filter_ext = "PNG Files (*.PNG);;" \
+                         "All Files (*.*)"
+        else:
+            filter_ext = "PDF Files (*.PDF);;" \
+                         "All Files (*.*)"
+
         try:
         try:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export SVG negative"),
+                caption=_("Export negative film"),
                 directory=self.app.get_last_save_folder() + '/' + name,
                 directory=self.app.get_last_save_folder() + '/' + name,
-                filter="*.svg")
+                filter=filter_ext)
         except TypeError:
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG negative"))
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export negative film"))
 
 
         filename = str(filename)
         filename = str(filename)
 
 
         if str(filename) == "":
         if str(filename) == "":
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export SVG negative cancelled."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export negative film cancelled."))
             return
             return
         else:
         else:
-            self.app.export_svg_negative(name, boxname, filename, border,
-                                         scale_stroke_factor=factor,
-                                         scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
-                                         skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
-                                         skew_reference=skew_reference,
-                                         mirror=mirror
-                                         )
+            self.export_negative(name, boxname, filename, border,
+                                 scale_stroke_factor=factor,
+                                 scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                 skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                 skew_reference=skew_reference,
+                                 mirror=mirror, ftype=ftype
+                                 )
+
+    def export_negative(self, obj_name, box_name, filename, boundary,
+                        scale_stroke_factor=0.00,
+                        scale_factor_x=None, scale_factor_y=None,
+                        skew_factor_x=None, skew_factor_y=None, skew_reference='center',
+                        mirror=None,
+                        use_thread=True, ftype='svg'):
+        """
+        Exports a Geometry Object to an SVG file in negative.
+
+        :param obj_name: the name of the FlatCAM object to be saved as SVG
+        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
+        :param filename: Path to the SVG file to save to.
+        :param boundary: thickness of a black border to surround all the features
+        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
+        :param scale_factor_x: factor to scale the svg geometry on the X axis
+        :param scale_factor_y: factor to scale the svg geometry on the Y axis
+        :param skew_factor_x: factor to skew the svg geometry on the X axis
+        :param skew_factor_y: factor to skew the svg geometry on the Y axis
+        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
+        those are the 4 points of the bounding box of the geometry to be skewed.
+        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+        :param use_thread: if to be run in a separate thread; boolean
+        :return:
+        """
+        self.app.report_usage("export_negative()")
+
+        if filename is None:
+            filename = self.app.defaults["global_last_save_folder"]
+
+        self.app.log.debug("export_svg() negative")
+
+        try:
+            obj = self.app.collection.get_by_name(str(obj_name))
+        except Exception:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        try:
+            box = self.app.collection.get_by_name(str(box_name))
+        except Exception:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % box_name
+
+        if box is None:
+            self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
+            box = obj
+
+        def make_negative_film():
+            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
+                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                          mirror=mirror
+                                          )
+
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            size = box.size()
+
+            uom = obj.units.lower()
+
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0] + (2 * boundary))
+            svgheight = str(size[1] + (2 * boundary))
+            minx = str(bounds[0] - boundary)
+            miny = str(bounds[1] + boundary + size[1])
+            miny_rect = str(bounds[1] - boundary)
+
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+            svg_header += '>'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
+
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width - wrong, we do when we have lines with certain width
+            # We set opacity to maximum
+            # We set the color to WHITE
+            root = ET.fromstring(exported_svg)
+            for child in root:
+                child.set('fill', '#FFFFFF')
+                child.set('opacity', '1.0')
+                child.set('stroke', '#FFFFFF')
+
+            # first_svg_elem = 'rect x="' + minx + '" ' + 'y="' + miny_rect + '" '
+            # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
+            # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
+
+            first_svg_elem_tag = 'rect'
+            first_svg_elem_attribs = {
+                'x': minx,
+                'y': miny_rect,
+                'width': svgwidth,
+                'height': svgheight,
+                'id': 'neg_rect',
+                'style': 'fill:#000000;opacity:1.0;stroke-width:0.0'
+            }
+
+            root.insert(0, ET.Element(first_svg_elem_tag, first_svg_elem_attribs))
+            exported_svg = ET.tostring(root)
+
+            svg_elem = svg_header + str(exported_svg) + svg_footer
+
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            doc_final = doc.toprettyxml()
+
+            if ftype == 'svg':
+                try:
+                    with open(filename, 'w') as fp:
+                        fp.write(doc_final)
+                except PermissionError:
+                    self.app.inform.emit('[WARNING] %s' %
+                                         _("Permission denied, saving not possible.\n"
+                                           "Most likely another app is holding the file open and not accessible."))
+                    return 'fail'
+            elif ftype == 'png':
+                try:
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+                    renderPM.drawToFile(drawing, filename, 'PNG')
+                except Exception as e:
+                    log.debug("FilmTool.export_negative() --> PNG output --> %s" % str(e))
+                    return 'fail'
+            else:
+                try:
+                    if self.units == 'INCH':
+                        from reportlab.lib.units import inch
+                        unit = inch
+                    else:
+                        from reportlab.lib.units import mm
+                        unit = mm
+
+                    doc_final = StringIO(doc_final)
+                    my_canvas = canvas.Canvas(filename, pagesize=A4)
+                    drawing = svg2rlg(doc_final)
+                    my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
+
+                    renderPDF.draw(drawing, my_canvas, 0, 0)
+                    my_canvas.save()
+                except Exception as e:
+                    log.debug("FilmTool.export_negative() --> PDF output --> %s" % str(e))
+                    return 'fail'
+
+            if self.app.defaults["global_open_style"] is False:
+                self.app.file_opened.emit("SVG", filename)
+            self.app.file_saved.emit("SVG", filename)
+            self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
+
+        if use_thread is True:
+            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
+
+            def job_thread_film(app_obj):
+                try:
+                    make_negative_film()
+                except Exception:
+                    proc.done()
+                    return
+                proc.done()
+
+            self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+        else:
+            make_negative_film()
+
+    def export_positive(self, obj_name, box_name, filename,
+                        scale_stroke_factor=0.00,
+                        scale_factor_x=None, scale_factor_y=None,
+                        skew_factor_x=None, skew_factor_y=None, skew_reference='center',
+                        mirror=None,
+                        use_thread=True, ftype='svg'):
+        """
+        Exports a Geometry Object to an SVG file in positive black.
+
+        :param obj_name: the name of the FlatCAM object to be saved as SVG
+        :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
+        :param filename: Path to the SVG file to save to.
+        :param scale_stroke_factor: factor by which to change/scale the thickness of the features
+        :param scale_factor_x: factor to scale the svg geometry on the X axis
+        :param scale_factor_y: factor to scale the svg geometry on the Y axis
+        :param skew_factor_x: factor to skew the svg geometry on the X axis
+        :param skew_factor_y: factor to skew the svg geometry on the Y axis
+        :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
+        those are the 4 points of the bounding box of the geometry to be skewed.
+        :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
+
+        :param use_thread: if to be run in a separate thread; boolean
+        :return:
+        """
+        self.app.report_usage("export_positive()")
+
+        if filename is None:
+            filename = self.app.defaults["global_last_save_folder"]
+
+        self.app.log.debug("export_svg() black")
+
+        try:
+            obj = self.app.collection.get_by_name(str(obj_name))
+        except Exception:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % obj_name
+
+        try:
+            box = self.app.collection.get_by_name(str(box_name))
+        except Exception:
+            # TODO: The return behavior has not been established... should raise exception?
+            return "Could not retrieve object: %s" % box_name
+
+        if box is None:
+            self.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
+            box = obj
+
+        def make_positive_film():
+            log.debug("FilmTool.export_positive().make_positive_film()")
+
+            exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
+                                          scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
+                                          skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
+                                          mirror=mirror
+                                          )
+
+            # Change the attributes of the exported SVG
+            # We don't need stroke-width
+            # We set opacity to maximum
+            # We set the colour to WHITE
+            root = ET.fromstring(exported_svg)
+            for child in root:
+                child.set('fill', str(self.app.defaults['tools_film_color']))
+                child.set('opacity', '1.0')
+                child.set('stroke', str(self.app.defaults['tools_film_color']))
+
+            exported_svg = ET.tostring(root)
+
+            # Determine bounding area for svg export
+            bounds = box.bounds()
+            size = box.size()
+
+            # This contain the measure units
+            uom = obj.units.lower()
+
+            # Define a boundary around SVG of about 1.0mm (~39mils)
+            if uom in "mm":
+                boundary = 1.0
+            else:
+                boundary = 0.0393701
+
+            # Convert everything to strings for use in the xml doc
+            svgwidth = str(size[0] + (2 * boundary))
+            svgheight = str(size[1] + (2 * boundary))
+            minx = str(bounds[0] - boundary)
+            miny = str(bounds[1] + boundary + size[1])
+
+            # Add a SVG Header and footer to the svg output from shapely
+            # The transform flips the Y Axis so that everything renders
+            # properly within svg apps such as inkscape
+            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
+                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
+            svg_header += 'width="' + svgwidth + uom + '" '
+            svg_header += 'height="' + svgheight + uom + '" '
+            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
+            svg_header += '>'
+            svg_header += '<g transform="scale(1,-1)">'
+            svg_footer = '</g> </svg>'
+
+            svg_elem = str(svg_header) + str(exported_svg) + str(svg_footer)
+
+            # Parse the xml through a xml parser just to add line feeds
+            # and to make it look more pretty for the output
+            doc = parse_xml_string(svg_elem)
+            doc_final = doc.toprettyxml()
+
+            if ftype == 'svg':
+                try:
+                    with open(filename, 'w') as fp:
+                        fp.write(doc_final)
+                except PermissionError:
+                    self.app.inform.emit('[WARNING] %s' %
+                                         _("Permission denied, saving not possible.\n"
+                                           "Most likely another app is holding the file open and not accessible."))
+                    return 'fail'
+            elif ftype == 'png':
+                try:
+                    doc_final = StringIO(doc_final)
+                    drawing = svg2rlg(doc_final)
+                    renderPM.drawToFile(drawing, filename, 'PNG')
+                except Exception as e:
+                    log.debug("FilmTool.export_positive() --> PNG output --> %s" % str(e))
+                    return 'fail'
+            else:
+                try:
+                    if self.units == 'INCH':
+                        from reportlab.lib.units import inch
+                        unit = inch
+                    else:
+                        from reportlab.lib.units import mm
+                        unit = mm
+
+                    doc_final = StringIO(doc_final)
+                    my_canvas = canvas.Canvas(filename, pagesize=A4)
+                    drawing = svg2rlg(doc_final)
+                    my_canvas.translate(bounds[0]*unit, bounds[1]*unit)
+
+                    renderPDF.draw(drawing, my_canvas, 0, 0)
+                    my_canvas.save()
+                except Exception as e:
+                    log.debug("FilmTool.export_positive() --> PDF output --> %s" % str(e))
+                    return 'fail'
+
+            if self.app.defaults["global_open_style"] is False:
+                self.app.file_opened.emit("SVG", filename)
+            self.app.file_saved.emit("SVG", filename)
+            self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
+
+        if use_thread is True:
+            proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
+
+            def job_thread_film(app_obj):
+                try:
+                    make_positive_film()
+                except Exception:
+                    proc.done()
+                    return
+                proc.done()
+
+            self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
+        else:
+            make_positive_film()
 
 
     def reset_fields(self):
     def reset_fields(self):
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 2 - 0
requirements.txt

@@ -22,3 +22,5 @@ rasterio
 lxml
 lxml
 ezdxf
 ezdxf
 qrcode>=6.0
 qrcode>=6.0
+reportlab>=3.0
+svglib

+ 1 - 1
setup_ubuntu.sh

@@ -5,4 +5,4 @@ sudo apt install --reinstall python3-pip python3-tk python3-imaging
 
 
 sudo python3 -m pip install --upgrade pip numpy scipy shapely rtree tk lxml cycler python-dateutil kiwisolver dill
 sudo python3 -m pip install --upgrade pip numpy scipy shapely rtree tk lxml cycler python-dateutil kiwisolver dill
 sudo python3 -m pip install --upgrade vispy pyopengl setuptools svg.path ortools freetype-py fontTools rasterio ezdxf
 sudo python3 -m pip install --upgrade vispy pyopengl setuptools svg.path ortools freetype-py fontTools rasterio ezdxf
-sudo python3 -m pip install --upgrade matplotlib qrcode
+sudo python3 -m pip install --upgrade matplotlib qrcode reportlab svglib