- PDF Import tool: added support for detection of circular geometry drawn with white color which means actually invisible color. When detected, FlatCAM will build an Excellon file out of those geoms.

- PDF Import tool: fixed storing geometries in apertures with the right size (before they were all stored in aperture D10)
This commit is contained in:
Marius Stanciu 2019-04-23 02:02:20 +03:00
parent d66d914cc3
commit 82a0287f4d
3 changed files with 228 additions and 63 deletions

View File

@ -95,7 +95,7 @@ class App(QtCore.QObject):
# Version
version = 8.914
version_date = "2019/04/22"
version_date = "2019/04/23"
beta = True
# current date now

View File

@ -16,7 +16,8 @@ CAD program, and create G-Code for Isolation routing.
- 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
- PDF Import tool: added support for PDF files that embed multiple Gerber layers (top, bottom, outline, silkscreen etc). Each will be opened in it's own Gerber file. The requirement is that each one is drawn in a different color
- PDF Import tool: fixed bugs when drag & dropping PDF files on canvas the files geometry previously opened was added to the new one. Also scaling issues. Solved.
- PDF Import tool: added support for detection of circular geometry drawn with white color which means actually invisible color. When detected, FlatCAM will build an Excellon file out of those geoms.
- PDF Import tool: fixed storing geometries in apertures with the right size (before they were all stored in aperture D10)
21.04.2019

View File

@ -43,8 +43,12 @@ class ToolPDF(FlatCAMTool):
self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
# detect color change; it means a new object to be created
self.color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*RG$')
# 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$')
@ -104,6 +108,7 @@ class ToolPDF(FlatCAMTool):
self.obj_dict = dict()
self.pdf_parsed = ''
self.parsed_obj_dict = dict()
# conversion factor to INCH
self.point_to_unit_factor = 0.01388888888
@ -151,6 +156,8 @@ class ToolPDF(FlatCAMTool):
new_name = filename.split('/')[-1].split('\\')[-1]
self.obj_dict.clear()
self.pdf_parsed = ''
self.parsed_obj_dict = {}
obj_type = 'gerber'
# 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.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
@ -174,37 +181,96 @@ class ToolPDF(FlatCAMTool):
except Exception as e:
log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
self.obj_dict = self.parse_pdf(pdf_content=self.pdf_parsed)
self.parsed_obj_dict = self.parse_pdf(pdf_content=self.pdf_parsed)
for k in self.obj_dict:
ap_dict = deepcopy(self.obj_dict[k])
for k in self.parsed_obj_dict:
ap_dict = deepcopy(self.parsed_obj_dict[k])
if ap_dict:
def obj_init(grb_obj, app_obj):
if k == 0:
# Excellon
obj_type = 'excellon'
grb_obj.apertures = ap_dict
new_name = new_name + "_exc"
# store the points here until reconstitution: keys are diameters and values are list of (x,y) coords
points = {}
poly_buff = []
for ap in grb_obj.apertures:
for k in grb_obj.apertures[ap]:
if k == 'solid_geometry':
poly_buff += ap_dict[ap][k]
def obj_init(exc_obj, app_obj):
# print(self.parsed_obj_dict[0])
poly_buff = unary_union(poly_buff)
poly_buff = poly_buff.buffer(0.0000001)
poly_buff = poly_buff.buffer(-0.0000001)
for geo in self.parsed_obj_dict[0]['0']['solid_geometry']:
xmin, ymin, xmax, ymax = geo.bounds
center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
grb_obj.solid_geometry = deepcopy(poly_buff)
# for drill bits, even in INCH, it's enough 3 decimals
correction_factor = 0.974
dia = (xmax - xmin) * correction_factor
dia = round(dia, 3)
if dia in points:
points[dia].append(center)
else:
points[dia] = [center]
with self.app.proc_container.new(_("Opening PDF layer #%d ...") % (int(k) - 2)):
sorted_dia = sorted(points.keys())
ret = self.app.new_object("gerber", new_name, obj_init, autoselected=False)
name_tool = 0
for dia in sorted_dia:
name_tool += 1
# create tools dictionary
spec = {"C": dia}
spec['solid_geometry'] = []
exc_obj.tools[str(name_tool)] = spec
# create drill list of dictionaries
for dia_points in points:
if dia == dia_points:
for pt in points[dia_points]:
exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
break
ret = exc_obj.create_geometry()
if ret == 'fail':
log.debug("Could not create geometry for Excellon object.")
return "fail"
for tool in exc_obj.tools:
if exc_obj.tools[tool]['solid_geometry']:
return
app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % new_name)
return "fail"
else:
# Gerber
obj_type = 'gerber'
def obj_init(grb_obj, app_obj):
grb_obj.apertures = ap_dict
poly_buff = []
for ap in grb_obj.apertures:
for k in grb_obj.apertures[ap]:
if k == 'solid_geometry':
poly_buff += ap_dict[ap][k]
poly_buff = unary_union(poly_buff)
try:
poly_buff = poly_buff.buffer(0.0000001)
except ValueError:
pass
try:
poly_buff = poly_buff.buffer(-0.0000001)
except ValueError:
pass
grb_obj.solid_geometry = deepcopy(poly_buff)
with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % (int(k) - 2)):
ret = self.app.new_object(obj_type, new_name, obj_init, autoselected=False)
if ret == 'fail':
self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
return
# Register recent file
self.app.file_opened.emit("gerber", filename)
self.app.file_opened.emit(obj_type, filename)
# GUI feedback
self.app.inform.emit(_("[success] Opened: %s") % filename)
@ -233,9 +299,6 @@ class ToolPDF(FlatCAMTool):
offset_geo = [0, 0]
scale_geo = [1, 1]
# initial aperture
aperture = 10
# store the objects to be transformed into Gerbers
object_dict = {}
@ -245,14 +308,29 @@ class ToolPDF(FlatCAMTool):
# 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 = dict()
# everything will be stored in the '0' aperture since we are dealing with clear polygons not strokes
clear_apertures_dict['0'] = dict()
clear_apertures_dict['0']['size'] = 0.0
clear_apertures_dict['0']['type'] = 'C'
clear_apertures_dict['0']['solid_geometry'] = []
# create first object
object_dict[object_nr] = apertures_dict
object_nr += 1
# on 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
# 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 object_nr = 0
flag_clear_geo = False
line_nr = 0
lines = pdf_content.splitlines()
@ -261,9 +339,12 @@ class ToolPDF(FlatCAMTool):
# log.debug("line %d: %s" % (line_nr, pline))
# COLOR DETECTION / OBJECT DETECTION
match = self.color_re.search(pline)
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
@ -271,9 +352,28 @@ class ToolPDF(FlatCAMTool):
else:
object_dict[object_nr] = deepcopy(apertures_dict)
object_nr += 1
object_dict[object_nr] = dict()
apertures_dict.clear()
apertures_dict = {}
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 #
@ -366,7 +466,7 @@ class ToolPDF(FlatCAMTool):
# add the start point to subpaths
subpath['lines'].append(start_point)
# subpath['bezier'].append(start_point)
subpath['rectangle'].append(start_point)
# subpath['rectangle'].append(start_point)
current_point = start_point
continue
@ -440,7 +540,7 @@ class ToolPDF(FlatCAMTool):
current_point = stop
continue
# Draw Rectangle 're
# Draw Rectangle 're'
match = self.rect_re.search(pline)
if match:
current_subpath = 'rectangle'
@ -454,7 +554,6 @@ class ToolPDF(FlatCAMTool):
pt2 = (x+width, y)
pt3 = (x+width, y+height)
pt4 = (x, y+height)
# 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
@ -491,7 +590,7 @@ class ToolPDF(FlatCAMTool):
path['bezier'].append(copy(subpath['bezier']))
subpath['bezier'] = []
elif current_subpath == 'rectangle':
subpath['rectangle'].append(start_point)
# 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'] = []
@ -566,12 +665,29 @@ class ToolPDF(FlatCAMTool):
path_geo.append(geo)
subpath['rectangle'] = []
try:
apertures_dict[str(aperture)]['solid_geometry'] += path_geo
except KeyError:
# in case there is no stroke width yet therefore no aperture
# 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:
apertures_dict[copy(found_aperture)]['solid_geometry'] += path_geo
found_aperture = None
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)]['solid_geometry'] = []
apertures_dict[str(aperture)]['solid_geometry'] += path_geo
else:
apertures_dict[str(aperture)] = {}
apertures_dict[str(aperture)]['size'] = applied_size
apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
apertures_dict[str(aperture)]['type'] = 'C'
apertures_dict[str(aperture)]['solid_geometry'] = []
apertures_dict[str(aperture)]['solid_geometry'] += path_geo
@ -583,8 +699,8 @@ class ToolPDF(FlatCAMTool):
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 = list()
if current_subpath == 'lines':
if path['lines']:
for subp in path['lines']:
@ -632,8 +748,8 @@ class ToolPDF(FlatCAMTool):
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])
if close_subpath is False and start_point is not None:
geo.append(start_point)
geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
path_geo.append(geo_el)
# the path was painted therefore initialize it
@ -650,24 +766,35 @@ class ToolPDF(FlatCAMTool):
# we finished painting and also closed the path if it was the case
close_subpath = True
try:
apertures_dict['0']['solid_geometry'] += path_geo
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']['solid_geometry'] = []
apertures_dict['0']['solid_geometry'] += path_geo
continue
# if there was a fill color change we look for circular geometries from which we can make drill holes
# for the Excellon file
if flag_clear_geo is True:
# we llok for circular geometries
if current_subpath == 'bezier':
# if there are geometries in the list
if path_geo:
clear_apertures_dict['0']['solid_geometry'] += path_geo
else:
# else, add the geometry as usual
try:
apertures_dict['0']['solid_geometry'] += path_geo
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']['solid_geometry'] = []
apertures_dict['0']['solid_geometry'] += path_geo
continue
# fill and stroke the path
# 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 = list()
fill_geo = list()
if current_subpath == 'lines':
if path['lines']:
# fill
@ -677,7 +804,7 @@ class ToolPDF(FlatCAMTool):
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)
fill_geo.append(geo_el)
# stroke
for subp in path['lines']:
geo = copy(subp)
@ -692,7 +819,7 @@ class ToolPDF(FlatCAMTool):
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)
fill_geo.append(geo_el)
# stroke
geo = copy(subpath['lines'])
geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
@ -711,7 +838,7 @@ class ToolPDF(FlatCAMTool):
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)
fill_geo.append(geo_el)
# stroke
for subp in path['bezier']:
geo = []
@ -728,7 +855,7 @@ class ToolPDF(FlatCAMTool):
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)
fill_geo.append(geo_el)
# stroke
geo = []
for b in subpath['bezier']:
@ -746,7 +873,7 @@ class ToolPDF(FlatCAMTool):
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)
fill_geo.append(geo_el)
# stroke
for subp in path['rectangle']:
geo = copy(subp)
@ -761,7 +888,7 @@ class ToolPDF(FlatCAMTool):
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)
fill_geo.append(geo_el)
# stroke
geo = copy(subpath['rectangle'])
geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
@ -771,16 +898,53 @@ class ToolPDF(FlatCAMTool):
# 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:
apertures_dict[copy(found_aperture)]['solid_geometry'] += path_geo
found_aperture = None
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)]['solid_geometry'] = []
apertures_dict[str(aperture)]['solid_geometry'] += path_geo
else:
apertures_dict[str(aperture)] = {}
apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
apertures_dict[str(aperture)]['type'] = 'C'
apertures_dict[str(aperture)]['solid_geometry'] = []
apertures_dict[str(aperture)]['solid_geometry'] += path_geo
# store the found geometry for filling the path
try:
apertures_dict['0']['solid_geometry'] += path_geo
apertures_dict['0']['solid_geometry'] += fill_geo
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']['size'] = round(applied_size, 5)
apertures_dict['0']['type'] = 'C'
apertures_dict['0']['solid_geometry'] = []
apertures_dict['0']['solid_geometry'] += path_geo
apertures_dict['0']['solid_geometry'] += fill_geo
continue
# tidy up. copy the current aperture dict to the object dict but only if it is not empty
if apertures_dict:
object_dict[object_nr] = deepcopy(apertures_dict)
if clear_apertures_dict['0']['solid_geometry']:
object_dict[0] = deepcopy(clear_apertures_dict)
return object_dict
def bezier_to_points(self, start, c1, c2, stop):