diff --git a/README.md b/README.md index acfa35c7..81d8384b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ CAD program, and create G-Code for Isolation routing. ================================================= +22.04.2019 + +- PDF import tool: added support for save/restore Graphics stack. Only for scale and offset transformations and for the linewidth. This is the final fix for Microsoft PDF printer who saves in PDF format 1.7 + 21.04.2019 - fixed the PDF import tool to work with files generated by the Microsoft PDF printer (chained subpaths) diff --git a/flatcamTools/ToolPDF.py b/flatcamTools/ToolPDF.py index 69536c28..638dd9e4 100644 --- a/flatcamTools/ToolPDF.py +++ b/flatcamTools/ToolPDF.py @@ -75,16 +75,30 @@ class ToolPDF(FlatCAMTool): 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$') + # 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$') + # 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*) ' + 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 = dict() + # 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.geo_buffer = [] self.pdf_parsed = '' @@ -201,8 +215,8 @@ class ToolPDF(FlatCAMTool): # store the start point (when 'm' command is encountered) current_subpath = None - # set True when 'h' command is encountered (close path) - close_path = False + # set True when 'h' command is encountered (close subpath) + close_subpath = False start_point = None current_point = None @@ -212,80 +226,94 @@ class ToolPDF(FlatCAMTool): offset_geo = [0, 0] scale_geo = [1, 1] - c_offset_f= [0, 0] - c_scale_f = [1, 1] - # initial aperture aperture = 10 # store the apertures here apertures_dict = {} - # it seems that first transform apply to the whole PDF; signal here if it's first - first_transform = True - line_nr = 0 lines = pdf_content.splitlines() for pline in lines: line_nr += 1 - log.debug("line %d: %s" % (line_nr, pline)) + # log.debug("line %d: %s" % (line_nr, pline)) # TRANSFORMATIONS DETECTION # - # # Detect Scale transform - # match = self.scale_re.search(pline) - # if match: - # log.debug( - # "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline)) - # if first_transform: - # first_transform = False - # c_scale_f = [float(match.group(1)), float(match.group(2))] - # else: - # scale_geo = [float(match.group(1)), float(match.group(2))] - # continue - - # # Detect Offset transform - # match = self.offset_re.search(pline) - # if match: - # log.debug( - # "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline)) - # offset_geo = [float(match.group(1)), float(match.group(2))] - # continue - - # Detect combined transformation. Must be always the last from transformations to be checked. + # 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(2)) == 0 and float(match.group(3)) == 0) and \ - (float(match.group(5)) != 0 or float(match.group(6)) != 0): + 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)) - if first_transform: - c_offset_f = [float(match.group(5)), float(match.group(6))] - else: - offset_geo = [float(match.group(5)), float(match.group(6))] + + 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(1)) != 1 and float(match.group(4)) != 1: + 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)) - if first_transform: - c_scale_f = [float(match.group(1)), float(match.group(4))] - else: - scale_geo = [float(match.group(1)), float(match.group(4))] - if first_transform: - first_transform = False + 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: + log.debug( + "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> %s" % (line_nr, pline)) + 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("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_path = False + close_subpath = False # init subpaths subpath['lines'] = [] @@ -295,8 +323,8 @@ class ToolPDF(FlatCAMTool): # 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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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 @@ -312,8 +340,8 @@ class ToolPDF(FlatCAMTool): 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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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 @@ -325,16 +353,16 @@ class ToolPDF(FlatCAMTool): 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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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 @@ -347,12 +375,12 @@ class ToolPDF(FlatCAMTool): 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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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 @@ -364,33 +392,33 @@ class ToolPDF(FlatCAMTool): 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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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] * c_scale_f[0], - y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[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]) print(subpath['bezier']) current_point = stop continue - # Draw RECTANGLE + # 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] * c_scale_f[0] - y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1] + 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] * c_scale_f[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] * c_scale_f[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) - # TODO: I'm not sure if rectangles are a subpath in themselves that autoclose + # TODO: I'm not sure if rectangles are a type of subpath that close by itself subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1] current_point = pt1 continue @@ -404,8 +432,8 @@ class ToolPDF(FlatCAMTool): subpath['rectangle'] = [] # it measns 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_path is True: - close_path = False + if close_subpath is True: + close_subpath = False if current_subpath == 'lines': path['lines'].pop(-1) if current_subpath == 'rectangle': @@ -415,7 +443,7 @@ class ToolPDF(FlatCAMTool): # Close SUBPATH match = self.end_subpath_re.search(pline) if match: - close_path = True + 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 @@ -439,24 +467,6 @@ class ToolPDF(FlatCAMTool): match = self.strokewidth_re.search(pline) if match: size = float(match.group(1)) - # flag = 0 - # - # if not apertures_dict: - # apertures_dict[str(aperture)] = dict() - # apertures_dict[str(aperture)]['size'] = size - # apertures_dict[str(aperture)]['type'] = 'C' - # apertures_dict[str(aperture)]['solid_geometry'] = [] - # else: - # for k in apertures_dict: - # if size == apertures_dict[k]['size']: - # flag = 1 - # break - # if flag == 0: - # aperture += 1 - # apertures_dict[str(aperture)] = dict() - # apertures_dict[str(aperture)]['size'] = size - # apertures_dict[str(aperture)]['type'] = 'C' - # apertures_dict[str(aperture)]['solid_geometry'] = [] continue # Detect No_Op command, ignore the current subpath @@ -471,7 +481,7 @@ class ToolPDF(FlatCAMTool): 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] * c_scale_f[0] * self.point_to_unit_factor + applied_size = size * scale_geo[0] * self.point_to_unit_factor path_geo = list() if current_subpath == 'lines': @@ -536,7 +546,7 @@ class ToolPDF(FlatCAMTool): 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] * c_scale_f[0] * self.point_to_unit_factor + applied_size = size * scale_geo[0] * self.point_to_unit_factor path_geo = list() if current_subpath == 'lines': @@ -544,7 +554,7 @@ class ToolPDF(FlatCAMTool): for subp in path['lines']: geo = copy(subp) # close the subpath if it was not closed already - if close_path is False: + if close_subpath is False: geo.append(geo[0]) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -553,7 +563,7 @@ class ToolPDF(FlatCAMTool): else: geo = copy(subpath['lines']) # close the subpath if it was not closed already - if close_path is False: + if close_subpath is False: geo.append(start_point) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -566,7 +576,7 @@ class ToolPDF(FlatCAMTool): 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_path is False: + if close_subpath is False: geo.append(geo[0]) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -575,7 +585,7 @@ class ToolPDF(FlatCAMTool): 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_path is False: + if close_subpath is False: geo.append(start_point) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -586,7 +596,7 @@ class ToolPDF(FlatCAMTool): for subp in path['rectangle']: geo = copy(subp) # close the subpath if it was not closed already - if close_path is False: + if close_subpath is False: geo.append(geo[0]) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -595,14 +605,14 @@ class ToolPDF(FlatCAMTool): else: geo = copy(subpath['rectangle']) # close the subpath if it was not closed already - if close_path is False: + if close_subpath is False: geo.append(start_point) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) subpath['rectangle'] = [] # we finished painting and also closed the path if it was the case - close_path = True + close_subpath = True try: apertures_dict['0']['solid_geometry'] += path_geo @@ -619,7 +629,7 @@ class ToolPDF(FlatCAMTool): 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] * c_scale_f[0] * self.point_to_unit_factor + applied_size = size * scale_geo[0] * self.point_to_unit_factor path_geo = list() if current_subpath == 'lines': @@ -628,7 +638,7 @@ class ToolPDF(FlatCAMTool): for subp in path['lines']: geo = copy(subp) # close the subpath if it was not closed already - if close_path is False: + if close_subpath is False: geo.append(geo[0]) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -643,7 +653,7 @@ class ToolPDF(FlatCAMTool): # fill geo = copy(subpath['lines']) # close the subpath if it was not closed already - if close_path is False: + if close_subpath is False: geo.append(start_point) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -662,7 +672,7 @@ class ToolPDF(FlatCAMTool): 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_path is False: + if close_subpath is False: geo.append(geo[0]) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -679,7 +689,7 @@ class ToolPDF(FlatCAMTool): # 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_path is False: + if close_subpath is False: geo.append(start_point) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -697,7 +707,7 @@ class ToolPDF(FlatCAMTool): for subp in path['rectangle']: geo = copy(subp) # close the subpath if it was not closed already - if close_path is False: + if close_subpath is False: geo.append(geo[0]) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -712,7 +722,7 @@ class ToolPDF(FlatCAMTool): # fill geo = copy(subpath['rectangle']) # close the subpath if it was not closed already - if close_path is False: + if close_subpath is False: geo.append(start_point) geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles) path_geo.append(geo_el) @@ -723,7 +733,7 @@ class ToolPDF(FlatCAMTool): subpath['rectangle'] = [] # we finished painting and also closed the path if it was the case - close_path = True + close_subpath = True try: apertures_dict['0']['solid_geometry'] += path_geo