diff --git a/FlatCAMApp.py b/FlatCAMApp.py index db593ebe..bbf73c4a 100644 --- a/FlatCAMApp.py +++ b/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 = ' 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() diff --git a/flatcamTools/ToolFilm.py b/flatcamTools/ToolFilm.py index d752b0c2..608ca359 100644 --- a/flatcamTools/ToolFilm.py +++ b/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 = ' 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 = ' 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())) diff --git a/requirements.txt b/requirements.txt index ca4310bf..8407fc7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,5 @@ rasterio lxml ezdxf qrcode>=6.0 +reportlab>=3.0 +svglib \ No newline at end of file diff --git a/setup_ubuntu.sh b/setup_ubuntu.sh index 3cc3cdab..4830b383 100644 --- a/setup_ubuntu.sh +++ b/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 \ No newline at end of file +sudo python3 -m pip install --upgrade matplotlib qrcode reportlab svglib \ No newline at end of file