- 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

@@ -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):