Explorar el Código

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

Marius Stanciu hace 6 años
padre
commit
f1af9d7999
Se han modificado 6 ficheros con 490 adiciones y 343 borrados
  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_array_size": 5,
             "gerber_editor_lin_axis": 'X',
-            "gerber_editor_lin_pitch": 1,
+            "gerber_editor_lin_pitch": 0.1,
             "gerber_editor_lin_angle": 0.0,
             "gerber_editor_circ_dir": 'CW',
             "gerber_editor_circ_angle": 0.0,
@@ -765,6 +765,7 @@ class App(QtCore.QObject):
             "tools_film_skew_ref_radio": 'bottomleft',
             "tools_film_mirror_cb": False,
             "tools_film_mirror_axis_radio": 'none',
+            "tools_film_file_type_radio": 'svg',
 
             # Panel Tool
             "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_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_file_type_radio": self.ui.tools_defaults_form.tools_film_group.file_type_radio,
 
             # Panelize Tool
             "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:
             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 new_units == self.defaults["units"].upper():
@@ -10059,291 +10061,6 @@ class App(QtCore.QObject):
             self.inform.emit('[success] %s: %s' %
                              (_("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):
         """
         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
 
 - 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, 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()
 
 

+ 458 - 55
flatcamTools/ToolFilm.py

@@ -15,6 +15,15 @@ from copy import deepcopy
 import logging
 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 FlatCAMTranslation as fcTranslate
 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.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
         self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
         self.film_skew_cb.setToolTip(
@@ -179,7 +194,7 @@ class Film(FlatCAMTool):
             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_entry = FCDoubleSpinner()
@@ -187,8 +202,8 @@ class Film(FlatCAMTool):
         self.film_skewx_entry.set_precision(self.decimals)
         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_entry = FCDoubleSpinner()
@@ -196,8 +211,8 @@ class Film(FlatCAMTool):
         self.film_skewy_entry.set_precision(self.decimals)
         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.setToolTip(
@@ -211,12 +226,18 @@ class Film(FlatCAMTool):
                                             orientation='vertical',
                                             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.film_skewy_label,  self.film_skewy_entry,
                                                                  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
         self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
         self.film_mirror_cb.setToolTip(
@@ -227,7 +248,7 @@ class Film(FlatCAMTool):
             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'},
                                           {'label': _('X'), 'value': 'x'},
@@ -236,13 +257,16 @@ class Film(FlatCAMTool):
                                          stretch=False)
         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.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
         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"
               "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
         self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
@@ -274,8 +298,8 @@ class Film(FlatCAMTool):
               "with white on a black canvas.\n"
               "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
         self.boundary_entry = FCDoubleSpinner()
@@ -294,8 +318,8 @@ class Film(FlatCAMTool):
               "white color like the rest and which may confound with the\n"
               "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_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"
                                    "the generated film is positive. This is done to help drilling,\n"
                                    "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
         self.punch_frame = QtWidgets.QFrame()
@@ -359,10 +383,32 @@ class Film(FlatCAMTool):
         self.punch_size_label.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.setToolTip(
             _("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"
               "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.units = self.app.defaults['units']
+
         # ## Signals
         self.film_object_button.clicked.connect(self.on_film_creation)
         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_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.file_type_radio.set_value(self.app.defaults["tools_film_file_type_radio"])
 
     def on_film_type(self, val):
         type_of_film = val
@@ -485,21 +534,21 @@ class Film(FlatCAMTool):
 
         try:
             name = self.tf_object_combo.currentText()
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("No FlatCAM object selected. Load an object for Film and retry."))
             return
 
         try:
             boxname = self.tf_box_combo.currentText()
-        except Exception as e:
+        except Exception:
             self.app.inform.emit('[ERROR_NOTCL] %s' %
                                  _("No FlatCAM object selected. Load an object for Box and retry."))
             return
 
         scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
-
         source = self.source_punch.get_value()
+        file_type = self.file_type_radio.get_value()
 
         # #################################################################
         # ################ STARTING THE JOB ###############################
@@ -510,13 +559,13 @@ class Film(FlatCAMTool):
         if self.film_type.get_value() == "pos":
 
             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:
-                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:
-            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 ...")
 
         scale_factor_x = None
@@ -541,29 +590,40 @@ class Film(FlatCAMTool):
         if self.film_mirror_cb.get_value():
             if self.film_mirror_axis.get_value() != 'none':
                 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:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export SVG positive"),
+                caption=_("Export positive film"),
                 directory=self.app.get_last_save_folder() + '/' + name,
-                filter="*.svg")
+                filter=filter_ext)
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG positive"))
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export positive film"))
 
         filename = 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
         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)
 
@@ -572,7 +632,7 @@ class Film(FlatCAMTool):
 
             try:
                 exc_name = self.exc_combo.currentText()
-            except Exception as e:
+            except Exception:
                 self.app.inform.emit('[ERROR_NOTCL] %s' %
                                      _("No Excellon object selected. Load an object for punching reference and retry."))
                 return
@@ -640,7 +700,7 @@ class Film(FlatCAMTool):
 
             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 ...")
 
         scale_factor_x = None
@@ -671,27 +731,370 @@ class Film(FlatCAMTool):
         if border is None:
             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:
             filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export SVG negative"),
+                caption=_("Export negative film"),
                 directory=self.app.get_last_save_folder() + '/' + name,
-                filter="*.svg")
+                filter=filter_ext)
         except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG negative"))
+            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export negative film"))
 
         filename = 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
         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):
         self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))

+ 2 - 0
requirements.txt

@@ -22,3 +22,5 @@ rasterio
 lxml
 ezdxf
 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 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