From 1085d26b7be24a7b5c5070272a0507dbad2236ef Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Mon, 18 May 2020 05:46:57 +0300 Subject: [PATCH] - fixed the PDF Tool when importing as Gerber objects - moved all the parsing out of the PDF Tool to a new file ParsePDF in the flatcamParsers folder - trying to fix the pixmap load crash when running a FlatCAMScript --- CHANGELOG.md | 6 + FlatCAMApp.py | 6 +- flatcamGUI/FlatCAMGUI.py | 26 +- flatcamObjects/FlatCAMScript.py | 11 +- flatcamParsers/ParsePDF.py | 1080 ++++++++++++++++++++++++++++ flatcamTools/ToolPDF.py | 1080 +--------------------------- tclCommands/TclCommandSetOrigin.py | 6 +- tclCommands/TclCommandSetPath.py | 13 +- 8 files changed, 1138 insertions(+), 1090 deletions(-) create mode 100644 flatcamParsers/ParsePDF.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 21c93fcf..a013677c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ CHANGELOG for FlatCAM beta ================================================= +18.05.2020 + +- fixed the PDF Tool when importing as Gerber objects +- moved all the parsing out of the PDF Tool to a new file ParsePDF in the flatcamParsers folder +- trying to fix the pixmap load crash when running a FlatCAMScript + 17.05.2020 - added new FlatCAM Tool: Corner Markers Tool which will add line markers in the selected corners of the bounding box of the targeted Gerber object diff --git a/FlatCAMApp.py b/FlatCAMApp.py index b5fa601b..9cb4c8dc 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -9810,16 +9810,18 @@ class App(QtCore.QObject): App.log.debug(" **************** Finished PROJECT loading... **************** ") - def plot_all(self, fit_view=True, use_thread=True): + def plot_all(self, fit_view=True, muted=False, use_thread=True): """ Re-generates all plots from all objects. :param fit_view: if True will plot the objects and will adjust the zoom to fit all plotted objects into view + :param muted: if True don't print messages :param use_thread: if True will use threading for plotting the objects :return: None """ self.log.debug("Plot_all()") - self.inform.emit('[success] %s...' % _("Redrawing all objects")) + if muted is not True: + self.inform.emit('[success] %s...' % _("Redrawing all objects")) for plot_obj in self.collection.get_list(): def worker_task(obj): diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index 193ec90e..44765738 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/flatcamGUI/FlatCAMGUI.py @@ -4300,6 +4300,8 @@ class FlatCAMInfoBar(QtWidgets.QWidget): self.pmap = QtGui.QPixmap(self.app.resource_location + '/graylight12.png') self.icon.setPixmap(self.pmap) + self.lock_pmaps = False + layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(5, 0, 5, 0) self.setLayout(layout) @@ -4311,7 +4313,6 @@ class FlatCAMInfoBar(QtWidgets.QWidget): self.text.setToolTip(_("Hello!")) layout.addWidget(self.text) - layout.addStretch() def set_text_(self, text, color=None): @@ -4323,17 +4324,18 @@ class FlatCAMInfoBar(QtWidgets.QWidget): def set_status(self, text, level="info"): level = str(level) - self.pmap.fill() - if level == "ERROR" or level == "ERROR_NOTCL": - self.pmap = QtGui.QPixmap(self.app.resource_location + '/redlight12.png') - elif level.lower() == "success": - self.pmap = QtGui.QPixmap(self.app.resource_location + '/greenlight12.png') - elif level == "WARNING" or level == "WARNING_NOTCL": - self.pmap = QtGui.QPixmap(self.app.resource_location + '/yellowlight12.png') - elif level.lower() == "selected": - self.pmap = QtGui.QPixmap(self.app.resource_location + '/bluelight12.png') - else: - self.pmap = QtGui.QPixmap(self.app.resource_location + '/graylight12.png') + if self.lock_pmaps is not True: + self.pmap.fill() + if level == "ERROR" or level == "ERROR_NOTCL": + self.pmap = QtGui.QPixmap(self.app.resource_location + '/redlight12.png') + elif level.lower() == "success": + self.pmap = QtGui.QPixmap(self.app.resource_location + '/greenlight12.png') + elif level == "WARNING" or level == "WARNING_NOTCL": + self.pmap = QtGui.QPixmap(self.app.resource_location + '/yellowlight12.png') + elif level.lower() == "selected": + self.pmap = QtGui.QPixmap(self.app.resource_location + '/bluelight12.png') + else: + self.pmap = QtGui.QPixmap(self.app.resource_location + '/graylight12.png') try: self.set_text_(text) diff --git a/flatcamObjects/FlatCAMScript.py b/flatcamObjects/FlatCAMScript.py index 68a6a423..e17df4d9 100644 --- a/flatcamObjects/FlatCAMScript.py +++ b/flatcamObjects/FlatCAMScript.py @@ -183,7 +183,13 @@ class ScriptObject(FlatCAMObj): if self.app.ui.shell_dock.isHidden(): self.app.ui.shell_dock.show() - self.script_code = deepcopy(self.script_editor_tab.code_editor.toPlainText()) + self.app.shell.open_processing() # Disables input box. + + # make sure that the pixmaps are not updated when running this as they will crash + # TODO find why the pixmaps load crash when run from this object (perhaps another thread?) + self.app.ui.fcinfo.lock_pmaps = True + + self.script_code = self.script_editor_tab.code_editor.toPlainText() old_line = '' for tcl_command_line in self.script_code.splitlines(): @@ -202,8 +208,6 @@ class ScriptObject(FlatCAMObj): # execute the actual Tcl command try: - self.app.shell.open_processing() # Disables input box. - result = self.app.shell.tcl.eval(str(new_command)) if result != 'None': self.app.shell.append_output(result + '\n') @@ -220,6 +224,7 @@ class ScriptObject(FlatCAMObj): log.error("Exec command Exception: %s\n" % result) self.app.shell.append_error('ERROR: %s\n '% result) + self.app.ui.fcinfo.lock_pmaps = False self.app.shell.close_processing() def on_autocomplete_changed(self, state): diff --git a/flatcamParsers/ParsePDF.py b/flatcamParsers/ParsePDF.py new file mode 100644 index 00000000..e7f189f3 --- /dev/null +++ b/flatcamParsers/ParsePDF.py @@ -0,0 +1,1080 @@ +# ########################################################## +# FlatCAM: 2D Post-processing for Manufacturing # +# File Author: Marius Adrian Stanciu (c) # +# Date: 4/23/2019 # +# MIT Licence # +# ########################################################## + +from PyQt5 import QtCore + +from FlatCAMCommon import GracefulException as grace + +from shapely.geometry import Polygon, LineString, MultiPolygon + +from copy import copy, deepcopy +import numpy as np +import re +import logging + +log = logging.getLogger('base') + + +class PdfParser(QtCore.QObject): + + def __init__(self, app): + super().__init__() + self.app = app + self.step_per_circles = self.app.defaults["gerber_circle_steps"] + + # detect stroke color change; it means a new object to be created + self.stroke_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*RG$') + + # detect fill color change; we check here for white color (transparent geometry); + # if detected we create an Excellon from it + self.fill_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*rg$') + + # detect 're' command + self.rect_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*re$') + # detect 'm' command + self.start_subpath_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sm$') + # detect 'l' command + self.draw_line_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sl') + # detect 'c' command + self.draw_arc_3pt_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)' + r'\s(-?\d+\.?\d*)\s*c$') + # detect 'v' command + self.draw_arc_2pt_c1start_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*v$') + # detect 'y' command + self.draw_arc_2pt_c2stop_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*y$') + # detect 'h' command + self.end_subpath_re = re.compile(r'^h$') + + # detect 'w' command + self.strokewidth_re = re.compile(r'^(\d+\.?\d*)\s*w$') + # detect 'S' command + self.stroke_path__re = re.compile(r'^S\s?[Q]?$') + # detect 's' command + self.close_stroke_path__re = re.compile(r'^s$') + # detect 'f' or 'f*' command + self.fill_path_re = re.compile(r'^[f|F][*]?$') + # detect 'B' or 'B*' command + self.fill_stroke_path_re = re.compile(r'^B[*]?$') + # detect 'b' or 'b*' command + self.close_fill_stroke_path_re = re.compile(r'^b[*]?$') + # detect 'n' + self.no_op_re = re.compile(r'^n$') + + # detect offset transformation. Pattern: (1) (0) (0) (1) (x) (y) + # self.offset_re = re.compile(r'^1\.?0*\s0?\.?0*\s0?\.?0*\s1\.?0*\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*cm$') + # detect scale transformation. Pattern: (factor_x) (0) (0) (factor_y) (0) (0) + # self.scale_re = re.compile(r'^q? (-?\d+\.?\d*) 0\.?0* 0\.?0* (-?\d+\.?\d*) 0\.?0* 0\.?0*\s+cm$') + # detect combined transformation. Should always be the last + self.combined_transform_re = re.compile(r'^(q)?\s*(-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) ' + r'(-?\d+\.?\d*) (-?\d+\.?\d*)\s+cm$') + + # detect clipping path + self.clip_path_re = re.compile(r'^W[*]? n?$') + + # detect save graphic state in graphic stack + self.save_gs_re = re.compile(r'^q.*?$') + + # detect restore graphic state from graphic stack + self.restore_gs_re = re.compile(r'^.*Q.*$') + + # graphic stack where we save parameters like transformation, line_width + self.gs = {} + # each element is a list composed of sublist elements + # (each sublist has 2 lists each having 2 elements: first is offset like: + # offset_geo = [off_x, off_y], second element is scale list with 2 elements, like: scale_geo = [sc_x, sc_yy]) + self.gs['transform'] = [] + self.gs['line_width'] = [] # each element is a float + + # conversion factor to INCH + self.point_to_unit_factor = 0.01388888888 + + def parse_pdf(self, pdf_content): + + # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH) + if self.app.defaults['units'].upper() == 'MM': + # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm + self.point_to_unit_factor = 25.4 / 72 + else: + # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch + self.point_to_unit_factor = 1 / 72 + + path = {} + path['lines'] = [] # it's a list of lines subpaths + path['bezier'] = [] # it's a list of bezier arcs subpaths + path['rectangle'] = [] # it's a list of rectangle subpaths + + subpath = {} + subpath['lines'] = [] # it's a list of points + subpath['bezier'] = [] # it's a list of sublists each like this [start, c1, c2, stop] + subpath['rectangle'] = [] # it's a list of sublists of points + + # store the start point (when 'm' command is encountered) + current_subpath = None + + # set True when 'h' command is encountered (close subpath) + close_subpath = False + + start_point = None + current_point = None + size = 0 + + # initial values for the transformations, in case they are not encountered in the PDF file + offset_geo = [0, 0] + scale_geo = [1, 1] + + # store the objects to be transformed into Gerbers + object_dict = {} + # will serve as key in the object_dict + layer_nr = 1 + # create first object + object_dict[layer_nr] = {} + + # store the apertures here + apertures_dict = {} + + # initial aperture + aperture = 10 + + # store the apertures with clear geometry here + # we are interested only in the circular geometry (drill holes) therefore we target only Bezier subpaths + clear_apertures_dict = {} + # everything will be stored in the '0' aperture since we are dealing with clear polygons not strokes + clear_apertures_dict['0'] = {} + clear_apertures_dict['0']['size'] = 0.0 + clear_apertures_dict['0']['type'] = 'C' + clear_apertures_dict['0']['geometry'] = [] + + # on stroke color change we create a new apertures dictionary and store the old one in a storage from where + # it will be transformed into Gerber object + old_color = [None, None, None] + + # signal that we have clear geometry and the geometry will be added to a special layer_nr = 0 + flag_clear_geo = False + + line_nr = 0 + lines = pdf_content.splitlines() + + for pline in lines: + if self.app.abort_flag: + # graceful abort requested by the user + raise grace + + line_nr += 1 + log.debug("line %d: %s" % (line_nr, pline)) + + # COLOR DETECTION / OBJECT DETECTION + match = self.stroke_color_re.search(pline) + if match: + color = [float(match.group(1)), float(match.group(2)), float(match.group(3))] + log.debug( + "ToolPDF.parse_pdf() --> STROKE Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" % + (line_nr, color[0], color[1], color[2])) + + if color[0] == old_color[0] and color[1] == old_color[1] and color[2] == old_color[2]: + # same color, do nothing + continue + else: + if apertures_dict: + object_dict[layer_nr] = deepcopy(apertures_dict) + apertures_dict.clear() + layer_nr += 1 + + object_dict[layer_nr] = {} + old_color = copy(color) + # we make sure that the following geometry is added to the right storage + flag_clear_geo = False + continue + + # CLEAR GEOMETRY detection + match = self.fill_color_re.search(pline) + if match: + fill_color = [float(match.group(1)), float(match.group(2)), float(match.group(3))] + log.debug( + "ToolPDF.parse_pdf() --> FILL Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" % + (line_nr, fill_color[0], fill_color[1], fill_color[2])) + # if the color is white we are seeing 'clear_geometry' that can't be seen. It may be that those + # geometries are actually holes from which we can make an Excellon file + if fill_color[0] == 1 and fill_color[1] == 1 and fill_color[2] == 1: + flag_clear_geo = True + else: + flag_clear_geo = False + continue + + # TRANSFORMATIONS DETECTION # + + # Detect combined transformation. + match = self.combined_transform_re.search(pline) + if match: + # detect save graphic stack event + # sometimes they combine save_to_graphics_stack with the transformation on the same line + if match.group(1) == 'q': + log.debug( + "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" % + (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1])) + + self.gs['transform'].append(deepcopy([offset_geo, scale_geo])) + self.gs['line_width'].append(deepcopy(size)) + + # transformation = TRANSLATION (OFFSET) + if (float(match.group(3)) == 0 and float(match.group(4)) == 0) and \ + (float(match.group(6)) != 0 or float(match.group(7)) != 0): + log.debug( + "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline)) + + offset_geo[0] += float(match.group(6)) + offset_geo[1] += float(match.group(7)) + # log.debug("Offset= [%f, %f]" % (offset_geo[0], offset_geo[1])) + + # transformation = SCALING + if float(match.group(2)) != 1 and float(match.group(5)) != 1: + log.debug( + "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline)) + + scale_geo[0] *= float(match.group(2)) + scale_geo[1] *= float(match.group(5)) + # log.debug("Scale= [%f, %f]" % (scale_geo[0], scale_geo[1])) + + continue + + # detect save graphic stack event + match = self.save_gs_re.search(pline) + if match: + log.debug( + "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" % + (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1])) + self.gs['transform'].append(deepcopy([offset_geo, scale_geo])) + self.gs['line_width'].append(deepcopy(size)) + + # detect restore from graphic stack event + match = self.restore_gs_re.search(pline) + if match: + try: + restored_transform = self.gs['transform'].pop(-1) + offset_geo = restored_transform[0] + scale_geo = restored_transform[1] + except IndexError: + # nothing to remove + log.debug("ToolPDF.parse_pdf() --> Nothing to restore") + pass + + try: + size = self.gs['line_width'].pop(-1) + except IndexError: + log.debug("ToolPDF.parse_pdf() --> Nothing to restore") + # nothing to remove + pass + + log.debug( + "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> " + "restored_offset=[%f, %f] ||| restored_scale=[%f, %f]" % + (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1])) + # log.debug("Restored Offset= [%f, %f]" % (offset_geo[0], offset_geo[1])) + # log.debug("Restored Scale= [%f, %f]" % (scale_geo[0], scale_geo[1])) + + # PATH CONSTRUCTION # + + # Start SUBPATH + match = self.start_subpath_re.search(pline) + if match: + # we just started a subpath so we mark it as not closed yet + close_subpath = False + + # init subpaths + subpath['lines'] = [] + subpath['bezier'] = [] + subpath['rectangle'] = [] + + # detect start point to move to + x = float(match.group(1)) + offset_geo[0] + y = float(match.group(2)) + offset_geo[1] + pt = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + start_point = pt + + # add the start point to subpaths + subpath['lines'].append(start_point) + # subpath['bezier'].append(start_point) + # subpath['rectangle'].append(start_point) + current_point = start_point + continue + + # Draw Line + match = self.draw_line_re.search(pline) + if match: + current_subpath = 'lines' + x = float(match.group(1)) + offset_geo[0] + y = float(match.group(2)) + offset_geo[1] + pt = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + subpath['lines'].append(pt) + current_point = pt + continue + + # Draw Bezier 'c' + match = self.draw_arc_3pt_re.search(pline) + if match: + current_subpath = 'bezier' + start = current_point + x = float(match.group(1)) + offset_geo[0] + y = float(match.group(2)) + offset_geo[1] + c1 = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + x = float(match.group(3)) + offset_geo[0] + y = float(match.group(4)) + offset_geo[1] + c2 = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + x = float(match.group(5)) + offset_geo[0] + y = float(match.group(6)) + offset_geo[1] + stop = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + + subpath['bezier'].append([start, c1, c2, stop]) + current_point = stop + continue + + # Draw Bezier 'v' + match = self.draw_arc_2pt_c1start_re.search(pline) + if match: + current_subpath = 'bezier' + start = current_point + x = float(match.group(1)) + offset_geo[0] + y = float(match.group(2)) + offset_geo[1] + c2 = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + x = float(match.group(3)) + offset_geo[0] + y = float(match.group(4)) + offset_geo[1] + stop = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + + subpath['bezier'].append([start, start, c2, stop]) + current_point = stop + continue + + # Draw Bezier 'y' + match = self.draw_arc_2pt_c2stop_re.search(pline) + if match: + start = current_point + x = float(match.group(1)) + offset_geo[0] + y = float(match.group(2)) + offset_geo[1] + c1 = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + x = float(match.group(3)) + offset_geo[0] + y = float(match.group(4)) + offset_geo[1] + stop = (x * self.point_to_unit_factor * scale_geo[0], + y * self.point_to_unit_factor * scale_geo[1]) + + subpath['bezier'].append([start, c1, stop, stop]) + current_point = stop + continue + + # Draw Rectangle 're' + match = self.rect_re.search(pline) + if match: + current_subpath = 'rectangle' + x = (float(match.group(1)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0] + y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1] + width = (float(match.group(3)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0] + height = (float(match.group(4)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1] + pt1 = (x, y) + pt2 = (x + width, y) + pt3 = (x + width, y + height) + pt4 = (x, y + height) + subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1] + current_point = pt1 + continue + + # Detect clipping path set + # ignore this and delete the current subpath + match = self.clip_path_re.search(pline) + if match: + subpath['lines'] = [] + subpath['bezier'] = [] + subpath['rectangle'] = [] + # it means that we've already added the subpath to path and we need to delete it + # clipping path is usually either rectangle or lines + if close_subpath is True: + close_subpath = False + if current_subpath == 'lines': + path['lines'].pop(-1) + if current_subpath == 'rectangle': + path['rectangle'].pop(-1) + continue + + # Close SUBPATH + match = self.end_subpath_re.search(pline) + if match: + close_subpath = True + if current_subpath == 'lines': + subpath['lines'].append(start_point) + # since we are closing the subpath add it to the path, a path may have chained subpaths + path['lines'].append(copy(subpath['lines'])) + subpath['lines'] = [] + elif current_subpath == 'bezier': + # subpath['bezier'].append(start_point) + # since we are closing the subpath add it to the path, a path may have chained subpaths + path['bezier'].append(copy(subpath['bezier'])) + subpath['bezier'] = [] + elif current_subpath == 'rectangle': + # subpath['rectangle'].append(start_point) + # since we are closing the subpath add it to the path, a path may have chained subpaths + path['rectangle'].append(copy(subpath['rectangle'])) + subpath['rectangle'] = [] + continue + + # PATH PAINTING # + + # Detect Stroke width / aperture + match = self.strokewidth_re.search(pline) + if match: + size = float(match.group(1)) + continue + + # Detect No_Op command, ignore the current subpath + match = self.no_op_re.search(pline) + if match: + subpath['lines'] = [] + subpath['bezier'] = [] + subpath['rectangle'] = [] + continue + + # Stroke the path + match = self.stroke_path__re.search(pline) + if match: + # scale the size here; some PDF printers apply transformation after the size is declared + applied_size = size * scale_geo[0] * self.point_to_unit_factor + path_geo = [] + if current_subpath == 'lines': + if path['lines']: + for subp in path['lines']: + geo = copy(subp) + try: + geo = LineString(geo).buffer((float(applied_size) / 2), + resolution=self.step_per_circles) + path_geo.append(geo) + except ValueError: + pass + # the path was painted therefore initialize it + path['lines'] = [] + else: + geo = copy(subpath['lines']) + try: + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + except ValueError: + pass + subpath['lines'] = [] + + if current_subpath == 'bezier': + if path['bezier']: + for subp in path['bezier']: + geo = [] + for b in subp: + geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) + try: + geo = LineString(geo).buffer((float(applied_size) / 2), + resolution=self.step_per_circles) + path_geo.append(geo) + except ValueError: + pass + # the path was painted therefore initialize it + path['bezier'] = [] + else: + geo = [] + for b in subpath['bezier']: + geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) + try: + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + except ValueError: + pass + subpath['bezier'] = [] + + if current_subpath == 'rectangle': + if path['rectangle']: + for subp in path['rectangle']: + geo = copy(subp) + try: + geo = LineString(geo).buffer((float(applied_size) / 2), + resolution=self.step_per_circles) + path_geo.append(geo) + except ValueError: + pass + # the path was painted therefore initialize it + path['rectangle'] = [] + else: + geo = copy(subpath['rectangle']) + try: + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + except ValueError: + pass + subpath['rectangle'] = [] + + # store the found geometry + found_aperture = None + if apertures_dict: + for apid in apertures_dict: + # if we already have an aperture with the current size (rounded to 5 decimals) + if apertures_dict[apid]['size'] == round(applied_size, 5): + found_aperture = apid + break + + if found_aperture: + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el)) + else: + if str(aperture) in apertures_dict.keys(): + aperture += 1 + apertures_dict[str(aperture)] = {} + apertures_dict[str(aperture)]['size'] = round(applied_size, 5) + apertures_dict[str(aperture)]['type'] = 'C' + apertures_dict[str(aperture)]['geometry'] = [] + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) + else: + apertures_dict[str(aperture)] = {} + apertures_dict[str(aperture)]['size'] = round(applied_size, 5) + apertures_dict[str(aperture)]['type'] = 'C' + apertures_dict[str(aperture)]['geometry'] = [] + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) + + continue + + # Fill the path + match = self.fill_path_re.search(pline) + if match: + # scale the size here; some PDF printers apply transformation after the size is declared + applied_size = size * scale_geo[0] * self.point_to_unit_factor + path_geo = [] + + if current_subpath == 'lines': + if path['lines']: + for subp in path['lines']: + geo = copy(subp) + # close the subpath if it was not closed already + if close_subpath is False: + geo.append(geo[0]) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + path_geo.append(geo_el) + except ValueError: + pass + # the path was painted therefore initialize it + path['lines'] = [] + else: + geo = copy(subpath['lines']) + # close the subpath if it was not closed already + if close_subpath is False: + geo.append(start_point) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + path_geo.append(geo_el) + except ValueError: + pass + subpath['lines'] = [] + + if current_subpath == 'bezier': + geo = [] + if path['bezier']: + for subp in path['bezier']: + for b in subp: + geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) + # close the subpath if it was not closed already + if close_subpath is False: + new_g = geo[0] + geo.append(new_g) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + path_geo.append(geo_el) + except ValueError: + pass + # the path was painted therefore initialize it + path['bezier'] = [] + else: + for b in subpath['bezier']: + geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) + if close_subpath is False: + geo.append(start_point) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + path_geo.append(geo_el) + except ValueError: + pass + subpath['bezier'] = [] + + if current_subpath == 'rectangle': + if path['rectangle']: + for subp in path['rectangle']: + geo = copy(subp) + # # close the subpath if it was not closed already + # if close_subpath is False and start_point is not None: + # geo.append(start_point) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + path_geo.append(geo_el) + except ValueError: + pass + # the path was painted therefore initialize it + path['rectangle'] = [] + else: + geo = copy(subpath['rectangle']) + # # close the subpath if it was not closed already + # if close_subpath is False and start_point is not None: + # geo.append(start_point) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + path_geo.append(geo_el) + except ValueError: + pass + subpath['rectangle'] = [] + + # we finished painting and also closed the path if it was the case + close_subpath = True + + # in case that a color change to white (transparent) occurred + if flag_clear_geo is True: + # if there was a fill color change we look for circular geometries from which we can make + # drill holes for the Excellon file + if current_subpath == 'bezier': + # if there are geometries in the list + if path_geo: + try: + for g in path_geo: + new_el = {} + new_el['clear'] = g + clear_apertures_dict['0']['geometry'].append(new_el) + except TypeError: + new_el = {} + new_el['clear'] = path_geo + clear_apertures_dict['0']['geometry'].append(new_el) + + # now that we finished searching for drill holes (this is not very precise because holes in the + # polygon pours may appear as drill too, but .. hey you can't have it all ...) we add + # clear_geometry + try: + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['clear'] = poly + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['clear'] = pdf_geo + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + except KeyError: + # in case there is no stroke width yet therefore no aperture + apertures_dict['0'] = {} + apertures_dict['0']['size'] = applied_size + apertures_dict['0']['type'] = 'C' + apertures_dict['0']['geometry'] = [] + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['clear'] = poly + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['clear'] = pdf_geo + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + # else, add the geometry as usual + try: + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + except KeyError: + # in case there is no stroke width yet therefore no aperture + apertures_dict['0'] = {} + apertures_dict['0']['size'] = applied_size + apertures_dict['0']['type'] = 'C' + apertures_dict['0']['geometry'] = [] + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + continue + + # Fill and Stroke the path + match = self.fill_stroke_path_re.search(pline) + if match: + # scale the size here; some PDF printers apply transformation after the size is declared + applied_size = size * scale_geo[0] * self.point_to_unit_factor + path_geo = [] + fill_geo = [] + + if current_subpath == 'lines': + if path['lines']: + # fill + for subp in path['lines']: + geo = copy(subp) + # close the subpath if it was not closed already + if close_subpath is False: + geo.append(geo[0]) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + fill_geo.append(geo_el) + except ValueError: + pass + # stroke + for subp in path['lines']: + geo = copy(subp) + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + # the path was painted therefore initialize it + path['lines'] = [] + else: + # fill + geo = copy(subpath['lines']) + # close the subpath if it was not closed already + if close_subpath is False: + geo.append(start_point) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + fill_geo.append(geo_el) + except ValueError: + pass + # stroke + geo = copy(subpath['lines']) + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + subpath['lines'] = [] + subpath['lines'] = [] + + if current_subpath == 'bezier': + geo = [] + if path['bezier']: + # fill + for subp in path['bezier']: + for b in subp: + geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) + # close the subpath if it was not closed already + if close_subpath is False: + geo.append(geo[0]) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + fill_geo.append(geo_el) + except ValueError: + pass + # stroke + for subp in path['bezier']: + geo = [] + for b in subp: + geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + # the path was painted therefore initialize it + path['bezier'] = [] + else: + # fill + for b in subpath['bezier']: + geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) + if close_subpath is False: + geo.append(start_point) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + fill_geo.append(geo_el) + except ValueError: + pass + # stroke + geo = [] + for b in subpath['bezier']: + geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + subpath['bezier'] = [] + + if current_subpath == 'rectangle': + if path['rectangle']: + # fill + for subp in path['rectangle']: + geo = copy(subp) + # # close the subpath if it was not closed already + # if close_subpath is False: + # geo.append(geo[0]) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + fill_geo.append(geo_el) + except ValueError: + pass + # stroke + for subp in path['rectangle']: + geo = copy(subp) + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + # the path was painted therefore initialize it + path['rectangle'] = [] + else: + # fill + geo = copy(subpath['rectangle']) + # # close the subpath if it was not closed already + # if close_subpath is False: + # geo.append(start_point) + try: + geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) + fill_geo.append(geo_el) + except ValueError: + pass + # stroke + geo = copy(subpath['rectangle']) + geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) + path_geo.append(geo) + subpath['rectangle'] = [] + + # we finished painting and also closed the path if it was the case + close_subpath = True + + # store the found geometry for stroking the path + found_aperture = None + if apertures_dict: + for apid in apertures_dict: + # if we already have an aperture with the current size (rounded to 5 decimals) + if apertures_dict[apid]['size'] == round(applied_size, 5): + found_aperture = apid + break + + if found_aperture: + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el)) + else: + if str(aperture) in apertures_dict.keys(): + aperture += 1 + apertures_dict[str(aperture)] = {} + apertures_dict[str(aperture)]['size'] = round(applied_size, 5) + apertures_dict[str(aperture)]['type'] = 'C' + apertures_dict[str(aperture)]['geometry'] = [] + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) + else: + apertures_dict[str(aperture)] = {} + apertures_dict[str(aperture)]['size'] = round(applied_size, 5) + apertures_dict[str(aperture)]['type'] = 'C' + apertures_dict[str(aperture)]['geometry'] = [] + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) + + # ############################################# ## + # store the found geometry for filling the path # + # ############################################# ## + + # in case that a color change to white (transparent) occurred + if flag_clear_geo is True: + try: + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in fill_geo: + new_el = {} + new_el['clear'] = poly + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['clear'] = pdf_geo + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + except KeyError: + # in case there is no stroke width yet therefore no aperture + apertures_dict['0'] = {} + apertures_dict['0']['size'] = round(applied_size, 5) + apertures_dict['0']['type'] = 'C' + apertures_dict['0']['geometry'] = [] + for pdf_geo in fill_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['clear'] = poly + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['clear'] = pdf_geo + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + try: + for pdf_geo in path_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in fill_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + except KeyError: + # in case there is no stroke width yet therefore no aperture + apertures_dict['0'] = {} + apertures_dict['0']['size'] = round(applied_size, 5) + apertures_dict['0']['type'] = 'C' + apertures_dict['0']['geometry'] = [] + for pdf_geo in fill_geo: + if isinstance(pdf_geo, MultiPolygon): + for poly in pdf_geo: + new_el = {} + new_el['solid'] = poly + new_el['follow'] = poly.exterior + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + else: + new_el = {} + new_el['solid'] = pdf_geo + new_el['follow'] = pdf_geo.exterior + apertures_dict['0']['geometry'].append(deepcopy(new_el)) + + continue + + # tidy up. copy the current aperture dict to the object dict but only if it is not empty + if apertures_dict: + object_dict[layer_nr] = deepcopy(apertures_dict) + + if clear_apertures_dict['0']['geometry']: + object_dict[0] = deepcopy(clear_apertures_dict) + + # delete keys (layers) with empty values + empty_layers = [] + for layer in object_dict: + if not object_dict[layer]: + empty_layers.append(layer) + for x in empty_layers: + if x in object_dict: + object_dict.pop(x) + + if self.app.abort_flag: + # graceful abort requested by the user + raise grace + + return object_dict + + def bezier_to_points(self, start, c1, c2, stop): + """ + # Equation Bezier, page 184 PDF 1.4 reference + # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf + # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0 + # in the following equation: + # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2 + P3*t ** 3 + # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides + # with the final point P3. Intermediate values of t generate intermediate points along the curve. + # The curve does not, in general, pass through the two control points P1 and P2 + + :return: A list of point coordinates tuples (x, y) + """ + + # here we store the geometric points + points = [] + + nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles)) + for t in nr_points: + term_p0 = (1 - t) ** 3 + term_p1 = 3 * t * (1 - t) ** 2 + term_p2 = 3 * (1 - t) * t ** 2 + term_p3 = t ** 3 + + x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3 + y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3 + points.append([x, y]) + + return points + + # def bezier_to_circle(self, path): + # lst = [] + # for el in range(len(path)): + # if type(path) is list: + # for coord in path[el]: + # lst.append(coord) + # else: + # lst.append(el) + # + # if lst: + # minx = min(lst, key=lambda t: t[0])[0] + # miny = min(lst, key=lambda t: t[1])[1] + # maxx = max(lst, key=lambda t: t[0])[0] + # maxy = max(lst, key=lambda t: t[1])[1] + # center = (maxx-minx, maxy-miny) + # radius = (maxx-minx) / 2 + # return [center, radius] + # + # def circle_to_points(self, center, radius): + # geo = Point(center).buffer(radius, resolution=self.step_per_circles) + # return LineString(list(geo.exterior.coords)) + # diff --git a/flatcamTools/ToolPDF.py b/flatcamTools/ToolPDF.py index a21e0f7a..1530525a 100644 --- a/flatcamTools/ToolPDF.py +++ b/flatcamTools/ToolPDF.py @@ -9,12 +9,11 @@ from PyQt5 import QtWidgets, QtCore from FlatCAMTool import FlatCAMTool from FlatCAMCommon import GracefulException as grace - -from shapely.geometry import Point, Polygon, LineString, MultiPolygon +from flatcamParsers.ParsePDF import PdfParser +from shapely.geometry import Point, MultiPolygon from shapely.ops import unary_union -from copy import copy, deepcopy -import numpy as np +from copy import deepcopy import zlib import re @@ -45,73 +44,9 @@ class ToolPDF(FlatCAMTool): FlatCAMTool.__init__(self, app) self.app = app self.decimals = self.app.decimals - self.step_per_circles = self.app.defaults["gerber_circle_steps"] self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S) - # detect stroke color change; it means a new object to be created - self.stroke_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*RG$') - - # detect fill color change; we check here for white color (transparent geometry); - # if detected we create an Excellon from it - self.fill_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*rg$') - - # detect 're' command - self.rect_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*re$') - # detect 'm' command - self.start_subpath_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sm$') - # detect 'l' command - self.draw_line_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sl') - # detect 'c' command - self.draw_arc_3pt_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)' - r'\s(-?\d+\.?\d*)\s*c$') - # detect 'v' command - self.draw_arc_2pt_c1start_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*v$') - # detect 'y' command - self.draw_arc_2pt_c2stop_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*y$') - # detect 'h' command - self.end_subpath_re = re.compile(r'^h$') - - # detect 'w' command - self.strokewidth_re = re.compile(r'^(\d+\.?\d*)\s*w$') - # detect 'S' command - self.stroke_path__re = re.compile(r'^S\s?[Q]?$') - # detect 's' command - self.close_stroke_path__re = re.compile(r'^s$') - # detect 'f' or 'f*' command - self.fill_path_re = re.compile(r'^[f|F][*]?$') - # detect 'B' or 'B*' command - self.fill_stroke_path_re = re.compile(r'^B[*]?$') - # detect 'b' or 'b*' command - self.close_fill_stroke_path_re = re.compile(r'^b[*]?$') - # detect 'n' - self.no_op_re = re.compile(r'^n$') - - # detect offset transformation. Pattern: (1) (0) (0) (1) (x) (y) - # self.offset_re = re.compile(r'^1\.?0*\s0?\.?0*\s0?\.?0*\s1\.?0*\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*cm$') - # detect scale transformation. Pattern: (factor_x) (0) (0) (factor_y) (0) (0) - # self.scale_re = re.compile(r'^q? (-?\d+\.?\d*) 0\.?0* 0\.?0* (-?\d+\.?\d*) 0\.?0* 0\.?0*\s+cm$') - # detect combined transformation. Should always be the last - self.combined_transform_re = re.compile(r'^(q)?\s*(-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) ' - r'(-?\d+\.?\d*) (-?\d+\.?\d*)\s+cm$') - - # detect clipping path - self.clip_path_re = re.compile(r'^W[*]? n?$') - - # detect save graphic state in graphic stack - self.save_gs_re = re.compile(r'^q.*?$') - - # detect restore graphic state from graphic stack - self.restore_gs_re = re.compile(r'^.*Q.*$') - - # graphic stack where we save parameters like transformation, line_width - self.gs = {} - # each element is a list composed of sublist elements - # (each sublist has 2 lists each having 2 elements: first is offset like: - # offset_geo = [off_x, off_y], second element is scale list with 2 elements, like: scale_geo = [sc_x, sc_yy]) - self.gs['transform'] = [] - self.gs['line_width'] = [] # each element is a float - self.pdf_decompressed = {} # key = file name and extension @@ -125,8 +60,7 @@ class ToolPDF(FlatCAMTool): # when empty we start the layer rendering self.parsing_promises = [] - # conversion factor to INCH - self.point_to_unit_factor = 0.01388888888 + self.parser = PdfParser(app=self.app) def run(self, toggle=True): self.app.defaults.report_usage("ToolPDF()") @@ -174,20 +108,13 @@ class ToolPDF(FlatCAMTool): def open_pdf(self, filename): short_name = filename.split('/')[-1].split('\\')[-1] self.parsing_promises.append(short_name) + self.pdf_parsed[short_name] = {} self.pdf_parsed[short_name]['pdf'] = {} self.pdf_parsed[short_name]['filename'] = filename self.pdf_decompressed[short_name] = '' - # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH) - if self.app.defaults['units'].upper() == 'MM': - # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm - self.point_to_unit_factor = 25.4 / 72 - else: - # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch - self.point_to_unit_factor = 1 / 72 - if self.app.abort_flag: # graceful abort requested by the user raise grace @@ -208,9 +135,11 @@ class ToolPDF(FlatCAMTool): try: self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n') except Exception as e: + self.app.inform.emit('[ERROR_NOTCL] %s: %s\n%s' % (_("Failed to open"), str(filename), str(e))) log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e)) + return - self.pdf_parsed[short_name]['pdf'] = self.parse_pdf(pdf_content=self.pdf_decompressed[short_name]) + self.pdf_parsed[short_name]['pdf'] = self.parser.parse_pdf(pdf_content=self.pdf_decompressed[short_name]) # we used it, now we delete it self.pdf_decompressed[short_name] = '' @@ -271,27 +200,24 @@ class ToolPDF(FlatCAMTool): for tool in exc_obj.tools: if exc_obj.tools[tool]['solid_geometry']: return - app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % - (_("No geometry found in file"), outname)) + app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), outname)) return "fail" with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)): ret_val = self.app.new_object("excellon", outname, obj_init, autoselected=False) if ret_val == 'fail': - self.app.inform.emit('[ERROR_NOTCL] %s' % - _('Open PDF file failed.')) + self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.')) return # Register recent file self.app.file_opened.emit("excellon", filename) # GUI feedback - self.app.inform.emit('[success] %s: %s' % - (_("Rendered"), outname)) + self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname)) def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr): outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr) - def obj_init(grb_obj): + def obj_init(grb_obj, app_obj): grb_obj.apertures = ap_dict @@ -354,8 +280,7 @@ class ToolPDF(FlatCAMTool): ret = self.app.new_object('gerber', outname, obj_init, autoselected=False) if ret == 'fail': - self.app.inform.emit('[ERROR_NOTCL] %s' % - _('Open PDF file failed.')) + self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.')) return # Register recent file self.app.file_opened.emit('gerber', filename) @@ -398,6 +323,7 @@ class ToolPDF(FlatCAMTool): try: if not self.parsing_promises: self.check_thread.stop() + log.debug("PDF --> start rendering") # parsing finished start the layer rendering if self.pdf_parsed: obj_to_delete = [] @@ -415,6 +341,7 @@ class ToolPDF(FlatCAMTool): raise grace ap_dict = pdf_content[k] + print(k, ap_dict) if ap_dict: layer_nr = k if k == 0: @@ -432,980 +359,3 @@ class ToolPDF(FlatCAMTool): log.debug("ToolPDF --> Periodic check finished.") except Exception: traceback.print_exc() - - def parse_pdf(self, pdf_content): - path = {} - path['lines'] = [] # it's a list of lines subpaths - path['bezier'] = [] # it's a list of bezier arcs subpaths - path['rectangle'] = [] # it's a list of rectangle subpaths - - subpath = {} - subpath['lines'] = [] # it's a list of points - subpath['bezier'] = [] # it's a list of sublists each like this [start, c1, c2, stop] - subpath['rectangle'] = [] # it's a list of sublists of points - - # store the start point (when 'm' command is encountered) - current_subpath = None - - # set True when 'h' command is encountered (close subpath) - close_subpath = False - - start_point = None - current_point = None - size = 0 - - # initial values for the transformations, in case they are not encountered in the PDF file - offset_geo = [0, 0] - scale_geo = [1, 1] - - # store the objects to be transformed into Gerbers - object_dict = {} - # will serve as key in the object_dict - layer_nr = 1 - # create first object - object_dict[layer_nr] = {} - - # store the apertures here - apertures_dict = {} - - # initial aperture - aperture = 10 - - # store the apertures with clear geometry here - # we are interested only in the circular geometry (drill holes) therefore we target only Bezier subpaths - clear_apertures_dict = {} - # everything will be stored in the '0' aperture since we are dealing with clear polygons not strokes - clear_apertures_dict['0'] = {} - clear_apertures_dict['0']['size'] = 0.0 - clear_apertures_dict['0']['type'] = 'C' - clear_apertures_dict['0']['geometry'] = [] - - # on stroke color change we create a new apertures dictionary and store the old one in a storage from where - # it will be transformed into Gerber object - old_color = [None, None, None] - - # signal that we have clear geometry and the geometry will be added to a special layer_nr = 0 - flag_clear_geo = False - - line_nr = 0 - lines = pdf_content.splitlines() - - for pline in lines: - if self.app.abort_flag: - # graceful abort requested by the user - raise grace - - line_nr += 1 - log.debug("line %d: %s" % (line_nr, pline)) - - # COLOR DETECTION / OBJECT DETECTION - match = self.stroke_color_re.search(pline) - if match: - color = [float(match.group(1)), float(match.group(2)), float(match.group(3))] - log.debug( - "ToolPDF.parse_pdf() --> STROKE Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" % - (line_nr, color[0], color[1], color[2])) - - if color[0] == old_color[0] and color[1] == old_color[1] and color[2] == old_color[2]: - # same color, do nothing - continue - else: - if apertures_dict: - object_dict[layer_nr] = deepcopy(apertures_dict) - apertures_dict.clear() - layer_nr += 1 - - object_dict[layer_nr] = {} - old_color = copy(color) - # we make sure that the following geometry is added to the right storage - flag_clear_geo = False - continue - - # CLEAR GEOMETRY detection - match = self.fill_color_re.search(pline) - if match: - fill_color = [float(match.group(1)), float(match.group(2)), float(match.group(3))] - log.debug( - "ToolPDF.parse_pdf() --> FILL Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" % - (line_nr, fill_color[0], fill_color[1], fill_color[2])) - # if the color is white we are seeing 'clear_geometry' that can't be seen. It may be that those - # geometries are actually holes from which we can make an Excellon file - if fill_color[0] == 1 and fill_color[1] == 1 and fill_color[2] == 1: - flag_clear_geo = True - else: - flag_clear_geo = False - continue - - # TRANSFORMATIONS DETECTION # - - # Detect combined transformation. - match = self.combined_transform_re.search(pline) - if match: - # detect save graphic stack event - # sometimes they combine save_to_graphics_stack with the transformation on the same line - if match.group(1) == 'q': - log.debug( - "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" % - (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1])) - - self.gs['transform'].append(deepcopy([offset_geo, scale_geo])) - self.gs['line_width'].append(deepcopy(size)) - - # transformation = TRANSLATION (OFFSET) - if (float(match.group(3)) == 0 and float(match.group(4)) == 0) and \ - (float(match.group(6)) != 0 or float(match.group(7)) != 0): - log.debug( - "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline)) - - offset_geo[0] += float(match.group(6)) - offset_geo[1] += float(match.group(7)) - # log.debug("Offset= [%f, %f]" % (offset_geo[0], offset_geo[1])) - - # transformation = SCALING - if float(match.group(2)) != 1 and float(match.group(5)) != 1: - log.debug( - "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline)) - - scale_geo[0] *= float(match.group(2)) - scale_geo[1] *= float(match.group(5)) - # log.debug("Scale= [%f, %f]" % (scale_geo[0], scale_geo[1])) - - continue - - # detect save graphic stack event - match = self.save_gs_re.search(pline) - if match: - log.debug( - "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" % - (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1])) - self.gs['transform'].append(deepcopy([offset_geo, scale_geo])) - self.gs['line_width'].append(deepcopy(size)) - - # detect restore from graphic stack event - match = self.restore_gs_re.search(pline) - if match: - try: - restored_transform = self.gs['transform'].pop(-1) - offset_geo = restored_transform[0] - scale_geo = restored_transform[1] - except IndexError: - # nothing to remove - log.debug("ToolPDF.parse_pdf() --> Nothing to restore") - pass - - try: - size = self.gs['line_width'].pop(-1) - except IndexError: - log.debug("ToolPDF.parse_pdf() --> Nothing to restore") - # nothing to remove - pass - - log.debug( - "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> " - "restored_offset=[%f, %f] ||| restored_scale=[%f, %f]" % - (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1])) - # log.debug("Restored Offset= [%f, %f]" % (offset_geo[0], offset_geo[1])) - # log.debug("Restored Scale= [%f, %f]" % (scale_geo[0], scale_geo[1])) - - # PATH CONSTRUCTION # - - # Start SUBPATH - match = self.start_subpath_re.search(pline) - if match: - # we just started a subpath so we mark it as not closed yet - close_subpath = False - - # init subpaths - subpath['lines'] = [] - subpath['bezier'] = [] - subpath['rectangle'] = [] - - # detect start point to move to - x = float(match.group(1)) + offset_geo[0] - y = float(match.group(2)) + offset_geo[1] - pt = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - start_point = pt - - # add the start point to subpaths - subpath['lines'].append(start_point) - # subpath['bezier'].append(start_point) - # subpath['rectangle'].append(start_point) - current_point = start_point - continue - - # Draw Line - match = self.draw_line_re.search(pline) - if match: - current_subpath = 'lines' - x = float(match.group(1)) + offset_geo[0] - y = float(match.group(2)) + offset_geo[1] - pt = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - subpath['lines'].append(pt) - current_point = pt - continue - - # Draw Bezier 'c' - match = self.draw_arc_3pt_re.search(pline) - if match: - current_subpath = 'bezier' - start = current_point - x = float(match.group(1)) + offset_geo[0] - y = float(match.group(2)) + offset_geo[1] - c1 = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - x = float(match.group(3)) + offset_geo[0] - y = float(match.group(4)) + offset_geo[1] - c2 = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - x = float(match.group(5)) + offset_geo[0] - y = float(match.group(6)) + offset_geo[1] - stop = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - - subpath['bezier'].append([start, c1, c2, stop]) - current_point = stop - continue - - # Draw Bezier 'v' - match = self.draw_arc_2pt_c1start_re.search(pline) - if match: - current_subpath = 'bezier' - start = current_point - x = float(match.group(1)) + offset_geo[0] - y = float(match.group(2)) + offset_geo[1] - c2 = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - x = float(match.group(3)) + offset_geo[0] - y = float(match.group(4)) + offset_geo[1] - stop = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - - subpath['bezier'].append([start, start, c2, stop]) - current_point = stop - continue - - # Draw Bezier 'y' - match = self.draw_arc_2pt_c2stop_re.search(pline) - if match: - start = current_point - x = float(match.group(1)) + offset_geo[0] - y = float(match.group(2)) + offset_geo[1] - c1 = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - x = float(match.group(3)) + offset_geo[0] - y = float(match.group(4)) + offset_geo[1] - stop = (x * self.point_to_unit_factor * scale_geo[0], - y * self.point_to_unit_factor * scale_geo[1]) - - subpath['bezier'].append([start, c1, stop, stop]) - current_point = stop - continue - - # Draw Rectangle 're' - match = self.rect_re.search(pline) - if match: - current_subpath = 'rectangle' - x = (float(match.group(1)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0] - y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1] - width = (float(match.group(3)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0] - height = (float(match.group(4)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1] - pt1 = (x, y) - pt2 = (x+width, y) - pt3 = (x+width, y+height) - pt4 = (x, y+height) - subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1] - current_point = pt1 - continue - - # Detect clipping path set - # ignore this and delete the current subpath - match = self.clip_path_re.search(pline) - if match: - subpath['lines'] = [] - subpath['bezier'] = [] - subpath['rectangle'] = [] - # it means that we've already added the subpath to path and we need to delete it - # clipping path is usually either rectangle or lines - if close_subpath is True: - close_subpath = False - if current_subpath == 'lines': - path['lines'].pop(-1) - if current_subpath == 'rectangle': - path['rectangle'].pop(-1) - continue - - # Close SUBPATH - match = self.end_subpath_re.search(pline) - if match: - close_subpath = True - if current_subpath == 'lines': - subpath['lines'].append(start_point) - # since we are closing the subpath add it to the path, a path may have chained subpaths - path['lines'].append(copy(subpath['lines'])) - subpath['lines'] = [] - elif current_subpath == 'bezier': - # subpath['bezier'].append(start_point) - # since we are closing the subpath add it to the path, a path may have chained subpaths - path['bezier'].append(copy(subpath['bezier'])) - subpath['bezier'] = [] - elif current_subpath == 'rectangle': - # subpath['rectangle'].append(start_point) - # since we are closing the subpath add it to the path, a path may have chained subpaths - path['rectangle'].append(copy(subpath['rectangle'])) - subpath['rectangle'] = [] - continue - - # PATH PAINTING # - - # Detect Stroke width / aperture - match = self.strokewidth_re.search(pline) - if match: - size = float(match.group(1)) - continue - - # Detect No_Op command, ignore the current subpath - match = self.no_op_re.search(pline) - if match: - subpath['lines'] = [] - subpath['bezier'] = [] - subpath['rectangle'] = [] - continue - - # Stroke the path - match = self.stroke_path__re.search(pline) - if match: - # scale the size here; some PDF printers apply transformation after the size is declared - applied_size = size * scale_geo[0] * self.point_to_unit_factor - path_geo = [] - if current_subpath == 'lines': - if path['lines']: - for subp in path['lines']: - geo = copy(subp) - try: - geo = LineString(geo).buffer((float(applied_size) / 2), - resolution=self.step_per_circles) - path_geo.append(geo) - except ValueError: - pass - # the path was painted therefore initialize it - path['lines'] = [] - else: - geo = copy(subpath['lines']) - try: - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - except ValueError: - pass - subpath['lines'] = [] - - if current_subpath == 'bezier': - if path['bezier']: - for subp in path['bezier']: - geo = [] - for b in subp: - geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) - try: - geo = LineString(geo).buffer((float(applied_size) / 2), - resolution=self.step_per_circles) - path_geo.append(geo) - except ValueError: - pass - # the path was painted therefore initialize it - path['bezier'] = [] - else: - geo = [] - for b in subpath['bezier']: - geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) - try: - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - except ValueError: - pass - subpath['bezier'] = [] - - if current_subpath == 'rectangle': - if path['rectangle']: - for subp in path['rectangle']: - geo = copy(subp) - try: - geo = LineString(geo).buffer((float(applied_size) / 2), - resolution=self.step_per_circles) - path_geo.append(geo) - except ValueError: - pass - # the path was painted therefore initialize it - path['rectangle'] = [] - else: - geo = copy(subpath['rectangle']) - try: - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - except ValueError: - pass - subpath['rectangle'] = [] - - # store the found geometry - found_aperture = None - if apertures_dict: - for apid in apertures_dict: - # if we already have an aperture with the current size (rounded to 5 decimals) - if apertures_dict[apid]['size'] == round(applied_size, 5): - found_aperture = apid - break - - if found_aperture: - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el)) - else: - if str(aperture) in apertures_dict.keys(): - aperture += 1 - apertures_dict[str(aperture)] = {} - apertures_dict[str(aperture)]['size'] = round(applied_size, 5) - apertures_dict[str(aperture)]['type'] = 'C' - apertures_dict[str(aperture)]['geometry'] = [] - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) - else: - apertures_dict[str(aperture)] = {} - apertures_dict[str(aperture)]['size'] = round(applied_size, 5) - apertures_dict[str(aperture)]['type'] = 'C' - apertures_dict[str(aperture)]['geometry'] = [] - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) - - continue - - # Fill the path - match = self.fill_path_re.search(pline) - if match: - # scale the size here; some PDF printers apply transformation after the size is declared - applied_size = size * scale_geo[0] * self.point_to_unit_factor - path_geo = [] - - if current_subpath == 'lines': - if path['lines']: - for subp in path['lines']: - geo = copy(subp) - # close the subpath if it was not closed already - if close_subpath is False: - geo.append(geo[0]) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - path_geo.append(geo_el) - except ValueError: - pass - # the path was painted therefore initialize it - path['lines'] = [] - else: - geo = copy(subpath['lines']) - # close the subpath if it was not closed already - if close_subpath is False: - geo.append(start_point) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - path_geo.append(geo_el) - except ValueError: - pass - subpath['lines'] = [] - - if current_subpath == 'bezier': - geo = [] - if path['bezier']: - for subp in path['bezier']: - for b in subp: - geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) - # close the subpath if it was not closed already - if close_subpath is False: - geo.append(geo[0]) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - path_geo.append(geo_el) - except ValueError: - pass - # the path was painted therefore initialize it - path['bezier'] = [] - else: - for b in subpath['bezier']: - geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) - if close_subpath is False: - geo.append(start_point) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - path_geo.append(geo_el) - except ValueError: - pass - subpath['bezier'] = [] - - if current_subpath == 'rectangle': - if path['rectangle']: - for subp in path['rectangle']: - geo = copy(subp) - # # close the subpath if it was not closed already - # if close_subpath is False and start_point is not None: - # geo.append(start_point) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - path_geo.append(geo_el) - except ValueError: - pass - # the path was painted therefore initialize it - path['rectangle'] = [] - else: - geo = copy(subpath['rectangle']) - # # close the subpath if it was not closed already - # if close_subpath is False and start_point is not None: - # geo.append(start_point) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - path_geo.append(geo_el) - except ValueError: - pass - subpath['rectangle'] = [] - - # we finished painting and also closed the path if it was the case - close_subpath = True - - # in case that a color change to white (transparent) occurred - if flag_clear_geo is True: - # if there was a fill color change we look for circular geometries from which we can make - # drill holes for the Excellon file - if current_subpath == 'bezier': - # if there are geometries in the list - if path_geo: - try: - for g in path_geo: - new_el = {} - new_el['clear'] = g - clear_apertures_dict['0']['geometry'].append(new_el) - except TypeError: - new_el = {} - new_el['clear'] = path_geo - clear_apertures_dict['0']['geometry'].append(new_el) - - # now that we finished searching for drill holes (this is not very precise because holes in the - # polygon pours may appear as drill too, but .. hey you can't have it all ...) we add - # clear_geometry - try: - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['clear'] = poly - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['clear'] = pdf_geo - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - except KeyError: - # in case there is no stroke width yet therefore no aperture - apertures_dict['0'] = {} - apertures_dict['0']['size'] = applied_size - apertures_dict['0']['type'] = 'C' - apertures_dict['0']['geometry'] = [] - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['clear'] = poly - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['clear'] = pdf_geo - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - # else, add the geometry as usual - try: - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - except KeyError: - # in case there is no stroke width yet therefore no aperture - apertures_dict['0'] = {} - apertures_dict['0']['size'] = applied_size - apertures_dict['0']['type'] = 'C' - apertures_dict['0']['geometry'] = [] - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - continue - - # Fill and Stroke the path - match = self.fill_stroke_path_re.search(pline) - if match: - # scale the size here; some PDF printers apply transformation after the size is declared - applied_size = size * scale_geo[0] * self.point_to_unit_factor - path_geo = [] - fill_geo = [] - - if current_subpath == 'lines': - if path['lines']: - # fill - for subp in path['lines']: - geo = copy(subp) - # close the subpath if it was not closed already - if close_subpath is False: - geo.append(geo[0]) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - fill_geo.append(geo_el) - except ValueError: - pass - # stroke - for subp in path['lines']: - geo = copy(subp) - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - # the path was painted therefore initialize it - path['lines'] = [] - else: - # fill - geo = copy(subpath['lines']) - # close the subpath if it was not closed already - if close_subpath is False: - geo.append(start_point) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - fill_geo.append(geo_el) - except ValueError: - pass - # stroke - geo = copy(subpath['lines']) - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - subpath['lines'] = [] - subpath['lines'] = [] - - if current_subpath == 'bezier': - geo = [] - if path['bezier']: - # fill - for subp in path['bezier']: - for b in subp: - geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) - # close the subpath if it was not closed already - if close_subpath is False: - geo.append(geo[0]) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - fill_geo.append(geo_el) - except ValueError: - pass - # stroke - for subp in path['bezier']: - geo = [] - for b in subp: - geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - # the path was painted therefore initialize it - path['bezier'] = [] - else: - # fill - for b in subpath['bezier']: - geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) - if close_subpath is False: - geo.append(start_point) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - fill_geo.append(geo_el) - except ValueError: - pass - # stroke - geo = [] - for b in subpath['bezier']: - geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3]) - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - subpath['bezier'] = [] - - if current_subpath == 'rectangle': - if path['rectangle']: - # fill - for subp in path['rectangle']: - geo = copy(subp) - # # close the subpath if it was not closed already - # if close_subpath is False: - # geo.append(geo[0]) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - fill_geo.append(geo_el) - except ValueError: - pass - # stroke - for subp in path['rectangle']: - geo = copy(subp) - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - # the path was painted therefore initialize it - path['rectangle'] = [] - else: - # fill - geo = copy(subpath['rectangle']) - # # close the subpath if it was not closed already - # if close_subpath is False: - # geo.append(start_point) - try: - geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) - fill_geo.append(geo_el) - except ValueError: - pass - # stroke - geo = copy(subpath['rectangle']) - geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles) - path_geo.append(geo) - subpath['rectangle'] = [] - - # we finished painting and also closed the path if it was the case - close_subpath = True - - # store the found geometry for stroking the path - found_aperture = None - if apertures_dict: - for apid in apertures_dict: - # if we already have an aperture with the current size (rounded to 5 decimals) - if apertures_dict[apid]['size'] == round(applied_size, 5): - found_aperture = apid - break - - if found_aperture: - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el)) - else: - if str(aperture) in apertures_dict.keys(): - aperture += 1 - apertures_dict[str(aperture)] = {} - apertures_dict[str(aperture)]['size'] = round(applied_size, 5) - apertures_dict[str(aperture)]['type'] = 'C' - apertures_dict[str(aperture)]['geometry'] = [] - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) - else: - apertures_dict[str(aperture)] = {} - apertures_dict[str(aperture)]['size'] = round(applied_size, 5) - apertures_dict[str(aperture)]['type'] = 'C' - apertures_dict[str(aperture)]['geometry'] = [] - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el)) - - # ############################################# ## - # store the found geometry for filling the path # - # ############################################# ## - - # in case that a color change to white (transparent) occurred - if flag_clear_geo is True: - try: - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in fill_geo: - new_el = {} - new_el['clear'] = poly - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['clear'] = pdf_geo - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - except KeyError: - # in case there is no stroke width yet therefore no aperture - apertures_dict['0'] = {} - apertures_dict['0']['size'] = round(applied_size, 5) - apertures_dict['0']['type'] = 'C' - apertures_dict['0']['geometry'] = [] - for pdf_geo in fill_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['clear'] = poly - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['clear'] = pdf_geo - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - try: - for pdf_geo in path_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in fill_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - except KeyError: - # in case there is no stroke width yet therefore no aperture - apertures_dict['0'] = {} - apertures_dict['0']['size'] = round(applied_size, 5) - apertures_dict['0']['type'] = 'C' - apertures_dict['0']['geometry'] = [] - for pdf_geo in fill_geo: - if isinstance(pdf_geo, MultiPolygon): - for poly in pdf_geo: - new_el = {} - new_el['solid'] = poly - new_el['follow'] = poly.exterior - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - else: - new_el = {} - new_el['solid'] = pdf_geo - new_el['follow'] = pdf_geo.exterior - apertures_dict['0']['geometry'].append(deepcopy(new_el)) - - continue - - # tidy up. copy the current aperture dict to the object dict but only if it is not empty - if apertures_dict: - object_dict[layer_nr] = deepcopy(apertures_dict) - - if clear_apertures_dict['0']['geometry']: - object_dict[0] = deepcopy(clear_apertures_dict) - - # delete keys (layers) with empty values - empty_layers = [] - for layer in object_dict: - if not object_dict[layer]: - empty_layers.append(layer) - for x in empty_layers: - if x in object_dict: - object_dict.pop(x) - - if self.app.abort_flag: - # graceful abort requested by the user - raise grace - - return object_dict - - def bezier_to_points(self, start, c1, c2, stop): - """ - # Equation Bezier, page 184 PDF 1.4 reference - # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf - # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0 - # in the following equation: - # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2 + P3*t ** 3 - # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides - # with the final point P3. Intermediate values of t generate intermediate points along the curve. - # The curve does not, in general, pass through the two control points P1 and P2 - - :return: A list of point coordinates tuples (x, y) - """ - - # here we store the geometric points - points = [] - - nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles)) - for t in nr_points: - term_p0 = (1 - t) ** 3 - term_p1 = 3 * t * (1 - t) ** 2 - term_p2 = 3 * (1 - t) * t ** 2 - term_p3 = t ** 3 - - x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3 - y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3 - points.append([x, y]) - - return points - - # def bezier_to_circle(self, path): - # lst = [] - # for el in range(len(path)): - # if type(path) is list: - # for coord in path[el]: - # lst.append(coord) - # else: - # lst.append(el) - # - # if lst: - # minx = min(lst, key=lambda t: t[0])[0] - # miny = min(lst, key=lambda t: t[1])[1] - # maxx = max(lst, key=lambda t: t[0])[0] - # maxy = max(lst, key=lambda t: t[1])[1] - # center = (maxx-minx, maxy-miny) - # radius = (maxx-minx) / 2 - # return [center, radius] - # - # def circle_to_points(self, center, radius): - # geo = Point(center).buffer(radius, resolution=self.step_per_circles) - # return LineString(list(geo.exterior.coords)) - # diff --git a/tclCommands/TclCommandSetOrigin.py b/tclCommands/TclCommandSetOrigin.py index e801fd13..a0afeb08 100644 --- a/tclCommands/TclCommandSetOrigin.py +++ b/tclCommands/TclCommandSetOrigin.py @@ -98,6 +98,6 @@ class TclCommandSetOrigin(TclCommand): loc = [0, 0] self.app.on_set_zero_click(event=None, location=loc, noplot=True, use_thread=False) - self.app.inform.emit('[success] Tcl %s: %s' % - (_('Origin set by offsetting all loaded objects with '), - '{0:.4f}, {0:.4f}'.format(loc[0], loc[1]))) + msg = '[success] Tcl %s: %s' % (_('Origin set by offsetting all loaded objects with '), + '{0:.4f}, {0:.4f}'.format(loc[0], loc[1])) + self.app.shell_message(msg, success=True, show=False) diff --git a/tclCommands/TclCommandSetPath.py b/tclCommands/TclCommandSetPath.py index 953fd57d..79d6c325 100644 --- a/tclCommands/TclCommandSetPath.py +++ b/tclCommands/TclCommandSetPath.py @@ -75,19 +75,22 @@ class TclCommandSetPath(TclCommand): if path_isdir is False: path_isfile = os.path.isfile(path) if path_isfile: - self.app.inform.emit('[ERROR] %s: %s, %s' % ( + msg = '[ERROR] %s: %s, %s' % ( "The provided path", str(path), - "is a path to file and not a directory as expected.")) + "is a path to file and not a directory as expected.") + self.app.shell_message(msg, success=True, show=False) return "Failed. The Tcl command set_path was used but it was not a directory." else: - self.app.inform.emit('[ERROR] %s: %s, %s' % ( + msg = '[ERROR] %s: %s, %s' % ( "The provided path", str(path), - "do not exist. Check for typos.")) + "do not exist. Check for typos.") + self.app.shell_message(msg, success=True, show=False) return "Failed. The Tcl command set_path was used but it does not exist." cd_command = 'cd %s' % path self.app.shell.exec_command(cd_command, no_echo=False) self.app.defaults["global_tcl_path"] = str(path) - self.app.inform.emit('[success] %s: %s' % ("Relative path set to", str(path))) + msg = '[success] %s: %s' % ("Relative path set to", str(path)) + self.app.shell_message(msg, success=True, show=False)