ToolPDF.py 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408
  1. # ########################################################## ##
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrian Stanciu (c) #
  5. # Date: 4/23/2019 #
  6. # MIT Licence #
  7. # ########################################################## ##
  8. from FlatCAMTool import FlatCAMTool
  9. from shapely.geometry import Point, Polygon, LineString
  10. from shapely.ops import cascaded_union, unary_union
  11. from FlatCAMObj import *
  12. import math
  13. from copy import copy, deepcopy
  14. import numpy as np
  15. import zlib
  16. import re
  17. import time
  18. import gettext
  19. import FlatCAMTranslation as fcTranslate
  20. import builtins
  21. fcTranslate.apply_language('strings')
  22. if '_' not in builtins.__dict__:
  23. _ = gettext.gettext
  24. class ToolPDF(FlatCAMTool):
  25. """
  26. Parse a PDF file.
  27. Reference here: https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  28. Return a list of geometries
  29. """
  30. toolName = _("PDF Import Tool")
  31. def __init__(self, app):
  32. FlatCAMTool.__init__(self, app)
  33. self.app = app
  34. self.step_per_circles = self.app.defaults["gerber_circle_steps"]
  35. self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
  36. # detect stroke color change; it means a new object to be created
  37. self.stroke_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*RG$')
  38. # detect fill color change; we check here for white color (transparent geometry);
  39. # if detected we create an Excellon from it
  40. self.fill_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*rg$')
  41. # detect 're' command
  42. self.rect_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*re$')
  43. # detect 'm' command
  44. self.start_subpath_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sm$')
  45. # detect 'l' command
  46. self.draw_line_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sl')
  47. # detect 'c' command
  48. self.draw_arc_3pt_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)'
  49. r'\s(-?\d+\.?\d*)\s*c$')
  50. # detect 'v' command
  51. self.draw_arc_2pt_c1start_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*v$')
  52. # detect 'y' command
  53. self.draw_arc_2pt_c2stop_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*y$')
  54. # detect 'h' command
  55. self.end_subpath_re = re.compile(r'^h$')
  56. # detect 'w' command
  57. self.strokewidth_re = re.compile(r'^(\d+\.?\d*)\s*w$')
  58. # detect 'S' command
  59. self.stroke_path__re = re.compile(r'^S\s?[Q]?$')
  60. # detect 's' command
  61. self.close_stroke_path__re = re.compile(r'^s$')
  62. # detect 'f' or 'f*' command
  63. self.fill_path_re = re.compile(r'^[f|F][*]?$')
  64. # detect 'B' or 'B*' command
  65. self.fill_stroke_path_re = re.compile(r'^B[*]?$')
  66. # detect 'b' or 'b*' command
  67. self.close_fill_stroke_path_re = re.compile(r'^b[*]?$')
  68. # detect 'n'
  69. self.no_op_re = re.compile(r'^n$')
  70. # detect offset transformation. Pattern: (1) (0) (0) (1) (x) (y)
  71. # self.offset_re = re.compile(r'^1\.?0*\s0?\.?0*\s0?\.?0*\s1\.?0*\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*cm$')
  72. # detect scale transformation. Pattern: (factor_x) (0) (0) (factor_y) (0) (0)
  73. # self.scale_re = re.compile(r'^q? (-?\d+\.?\d*) 0\.?0* 0\.?0* (-?\d+\.?\d*) 0\.?0* 0\.?0*\s+cm$')
  74. # detect combined transformation. Should always be the last
  75. self.combined_transform_re = re.compile(r'^(q)?\s*(-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) '
  76. r'(-?\d+\.?\d*) (-?\d+\.?\d*)\s+cm$')
  77. # detect clipping path
  78. self.clip_path_re = re.compile(r'^W[*]? n?$')
  79. # detect save graphic state in graphic stack
  80. self.save_gs_re = re.compile(r'^q.*?$')
  81. # detect restore graphic state from graphic stack
  82. self.restore_gs_re = re.compile(r'^.*Q.*$')
  83. # graphic stack where we save parameters like transformation, line_width
  84. self.gs = dict()
  85. # each element is a list composed of sublist elements
  86. # (each sublist has 2 lists each having 2 elements: first is offset like:
  87. # offset_geo = [off_x, off_y], second element is scale list with 2 elements, like: scale_geo = [sc_x, sc_yy])
  88. self.gs['transform'] = []
  89. self.gs['line_width'] = [] # each element is a float
  90. self.pdf_decompressed = {}
  91. # key = file name and extension
  92. # value is a dict to store the parsed content of the PDF
  93. self.pdf_parsed = {}
  94. # QTimer for periodic check
  95. self.check_thread = QtCore.QTimer()
  96. # Every time a parser is started we add a promise; every time a parser finished we remove a promise
  97. # when empty we start the layer rendering
  98. self.parsing_promises = []
  99. # conversion factor to INCH
  100. self.point_to_unit_factor = 0.01388888888
  101. def run(self, toggle=True):
  102. self.app.report_usage("ToolPDF()")
  103. self.set_tool_ui()
  104. self.on_open_pdf_click()
  105. def install(self, icon=None, separator=None, **kwargs):
  106. FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
  107. def set_tool_ui(self):
  108. pass
  109. def on_open_pdf_click(self):
  110. """
  111. File menu callback for opening an PDF file.
  112. :return: None
  113. """
  114. self.app.report_usage("ToolPDF.on_open_pdf_click()")
  115. self.app.log.debug("ToolPDF.on_open_pdf_click()")
  116. _filter_ = "Adobe PDF Files (*.pdf);;" \
  117. "All Files (*.*)"
  118. try:
  119. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
  120. directory=self.app.get_last_folder(),
  121. filter=_filter_)
  122. except TypeError:
  123. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
  124. if len(filenames) == 0:
  125. self.app.inform.emit('[WARNING_NOTCL] %s.' % _("Open PDF cancelled"))
  126. else:
  127. # start the parsing timer with a period of 1 second
  128. self.periodic_check(1000)
  129. for filename in filenames:
  130. if filename != '':
  131. self.app.worker_task.emit({'fcn': self.open_pdf,
  132. 'params': [filename]})
  133. def open_pdf(self, filename):
  134. short_name = filename.split('/')[-1].split('\\')[-1]
  135. self.parsing_promises.append(short_name)
  136. self.pdf_parsed[short_name] = {}
  137. self.pdf_parsed[short_name]['pdf'] = {}
  138. self.pdf_parsed[short_name]['filename'] = filename
  139. self.pdf_decompressed[short_name] = ''
  140. # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
  141. if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  142. # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm
  143. self.point_to_unit_factor = 25.4 / 72
  144. else:
  145. # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch
  146. self.point_to_unit_factor = 1 / 72
  147. if self.app.abort_flag:
  148. # graceful abort requested by the user
  149. raise FlatCAMApp.GracefulException
  150. with self.app.proc_container.new(_("Parsing PDF file ...")):
  151. with open(filename, "rb") as f:
  152. pdf = f.read()
  153. stream_nr = 0
  154. for s in re.findall(self.stream_re, pdf):
  155. if self.app.abort_flag:
  156. # graceful abort requested by the user
  157. raise FlatCAMApp.GracefulException
  158. stream_nr += 1
  159. log.debug(" PDF STREAM: %d\n" % stream_nr)
  160. s = s.strip(b'\r\n')
  161. try:
  162. self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n')
  163. except Exception as e:
  164. log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
  165. self.pdf_parsed[short_name]['pdf'] = self.parse_pdf(pdf_content=self.pdf_decompressed[short_name])
  166. # we used it, now we delete it
  167. self.pdf_decompressed[short_name] = ''
  168. # removal from list is done in a multithreaded way therefore not always the removal can be done
  169. # try to remove until it's done
  170. try:
  171. while True:
  172. self.parsing_promises.remove(short_name)
  173. time.sleep(0.1)
  174. except Exception as e:
  175. log.debug("ToolPDF.open_pdf() --> %s" % str(e))
  176. self.app.inform.emit('[success] %s: %s' % (_("Opened"), str(filename)))
  177. def layer_rendering_as_excellon(self, filename, ap_dict, layer_nr):
  178. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  179. # store the points here until reconstitution:
  180. # keys are diameters and values are list of (x,y) coords
  181. points = {}
  182. def obj_init(exc_obj, app_obj):
  183. clear_geo = [geo_el['clear'] for geo_el in ap_dict['0']['geometry']]
  184. for geo in clear_geo:
  185. xmin, ymin, xmax, ymax = geo.bounds
  186. center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
  187. # for drill bits, even in INCH, it's enough 3 decimals
  188. correction_factor = 0.974
  189. dia = (xmax - xmin) * correction_factor
  190. dia = round(dia, 3)
  191. if dia in points:
  192. points[dia].append(center)
  193. else:
  194. points[dia] = [center]
  195. sorted_dia = sorted(points.keys())
  196. name_tool = 0
  197. for dia in sorted_dia:
  198. name_tool += 1
  199. # create tools dictionary
  200. spec = {"C": dia, 'solid_geometry': []}
  201. exc_obj.tools[str(name_tool)] = spec
  202. # create drill list of dictionaries
  203. for dia_points in points:
  204. if dia == dia_points:
  205. for pt in points[dia_points]:
  206. exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
  207. break
  208. ret = exc_obj.create_geometry()
  209. if ret == 'fail':
  210. log.debug("Could not create geometry for Excellon object.")
  211. return "fail"
  212. for tool in exc_obj.tools:
  213. if exc_obj.tools[tool]['solid_geometry']:
  214. return
  215. app_obj.inform.emit('[ERROR_NOTCL] %s: %s' %
  216. (_("No geometry found in file"), outname))
  217. return "fail"
  218. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  219. ret_val = self.app.new_object("excellon", outname, obj_init, autoselected=False)
  220. if ret_val == 'fail':
  221. self.app.inform.emit('[ERROR_NOTCL] %s' %
  222. _('Open PDF file failed.'))
  223. return
  224. # Register recent file
  225. self.app.file_opened.emit("excellon", filename)
  226. # GUI feedback
  227. self.app.inform.emit('[success] %s: %s' %
  228. (_("Rendered"), outname))
  229. def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
  230. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  231. def obj_init(grb_obj, app_obj):
  232. grb_obj.apertures = ap_dict
  233. poly_buff = []
  234. follow_buf = []
  235. for ap in grb_obj.apertures:
  236. for k in grb_obj.apertures[ap]:
  237. if k == 'geometry':
  238. for geo_el in ap_dict[ap][k]:
  239. if 'solid' in geo_el:
  240. poly_buff.append(geo_el['solid'])
  241. if 'follow' in geo_el:
  242. follow_buf.append(geo_el['follow'])
  243. poly_buff = unary_union(poly_buff)
  244. if '0' in grb_obj.apertures:
  245. global_clear_geo = []
  246. if 'geometry' in grb_obj.apertures['0']:
  247. for geo_el in ap_dict['0']['geometry']:
  248. if 'clear' in geo_el:
  249. global_clear_geo.append(geo_el['clear'])
  250. if global_clear_geo:
  251. solid = []
  252. for apid in grb_obj.apertures:
  253. if 'geometry' in grb_obj.apertures[apid]:
  254. for elem in grb_obj.apertures[apid]['geometry']:
  255. if 'solid' in elem:
  256. solid_geo = deepcopy(elem['solid'])
  257. for clear_geo in global_clear_geo:
  258. # Make sure that the clear_geo is within the solid_geo otherwise we loose
  259. # the solid_geometry. We want for clear_geometry just to cut into solid_geometry
  260. # not to delete it
  261. if clear_geo.within(solid_geo):
  262. solid_geo = solid_geo.difference(clear_geo)
  263. if solid_geo.is_empty:
  264. solid_geo = elem['solid']
  265. try:
  266. for poly in solid_geo:
  267. solid.append(poly)
  268. except TypeError:
  269. solid.append(solid_geo)
  270. poly_buff = deepcopy(MultiPolygon(solid))
  271. follow_buf = unary_union(follow_buf)
  272. try:
  273. poly_buff = poly_buff.buffer(0.0000001)
  274. except ValueError:
  275. pass
  276. try:
  277. poly_buff = poly_buff.buffer(-0.0000001)
  278. except ValueError:
  279. pass
  280. grb_obj.solid_geometry = deepcopy(poly_buff)
  281. grb_obj.follow_geometry = deepcopy(follow_buf)
  282. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  283. ret = self.app.new_object('gerber', outname, obj_init, autoselected=False)
  284. if ret == 'fail':
  285. self.app.inform.emit('[ERROR_NOTCL] %s' %
  286. _('Open PDF file failed.'))
  287. return
  288. # Register recent file
  289. self.app.file_opened.emit('gerber', filename)
  290. # GUI feedback
  291. self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
  292. def periodic_check(self, check_period):
  293. """
  294. This function starts an QTimer and it will periodically check if parsing was done
  295. :param check_period: time at which to check periodically if all plots finished to be plotted
  296. :return:
  297. """
  298. # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
  299. # self.plot_thread.start()
  300. log.debug("ToolPDF --> Periodic Check started.")
  301. try:
  302. self.check_thread.stop()
  303. except TypeError:
  304. pass
  305. self.check_thread.setInterval(check_period)
  306. try:
  307. self.check_thread.timeout.disconnect(self.periodic_check_handler)
  308. except (TypeError, AttributeError):
  309. pass
  310. self.check_thread.timeout.connect(self.periodic_check_handler)
  311. self.check_thread.start(QtCore.QThread.HighPriority)
  312. def periodic_check_handler(self):
  313. """
  314. If the parsing worker finished then start multithreaded rendering
  315. :return:
  316. """
  317. # log.debug("checking parsing --> %s" % str(self.parsing_promises))
  318. try:
  319. if not self.parsing_promises:
  320. self.check_thread.stop()
  321. # parsing finished start the layer rendering
  322. if self.pdf_parsed:
  323. obj_to_delete = []
  324. for object_name in self.pdf_parsed:
  325. if self.app.abort_flag:
  326. # graceful abort requested by the user
  327. raise FlatCAMApp.GracefulException
  328. filename = deepcopy(self.pdf_parsed[object_name]['filename'])
  329. pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
  330. obj_to_delete.append(object_name)
  331. for k in pdf_content:
  332. if self.app.abort_flag:
  333. # graceful abort requested by the user
  334. raise FlatCAMApp.GracefulException
  335. ap_dict = pdf_content[k]
  336. if ap_dict:
  337. layer_nr = k
  338. if k == 0:
  339. self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
  340. 'params': [filename, ap_dict, layer_nr]})
  341. else:
  342. self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
  343. 'params': [filename, ap_dict, layer_nr]})
  344. # delete the object already processed so it will not be processed again for other objects
  345. # that were opened at the same time; like in drag & drop on GUI
  346. for obj_name in obj_to_delete:
  347. if obj_name in self.pdf_parsed:
  348. self.pdf_parsed.pop(obj_name)
  349. log.debug("ToolPDF --> Periodic check finished.")
  350. except Exception:
  351. traceback.print_exc()
  352. def parse_pdf(self, pdf_content):
  353. path = dict()
  354. path['lines'] = [] # it's a list of lines subpaths
  355. path['bezier'] = [] # it's a list of bezier arcs subpaths
  356. path['rectangle'] = [] # it's a list of rectangle subpaths
  357. subpath = dict()
  358. subpath['lines'] = [] # it's a list of points
  359. subpath['bezier'] = [] # it's a list of sublists each like this [start, c1, c2, stop]
  360. subpath['rectangle'] = [] # it's a list of sublists of points
  361. # store the start point (when 'm' command is encountered)
  362. current_subpath = None
  363. # set True when 'h' command is encountered (close subpath)
  364. close_subpath = False
  365. start_point = None
  366. current_point = None
  367. size = 0
  368. # initial values for the transformations, in case they are not encountered in the PDF file
  369. offset_geo = [0, 0]
  370. scale_geo = [1, 1]
  371. # store the objects to be transformed into Gerbers
  372. object_dict = {}
  373. # will serve as key in the object_dict
  374. layer_nr = 1
  375. # create first object
  376. object_dict[layer_nr] = {}
  377. # store the apertures here
  378. apertures_dict = {}
  379. # initial aperture
  380. aperture = 10
  381. # store the apertures with clear geometry here
  382. # we are interested only in the circular geometry (drill holes) therefore we target only Bezier subpaths
  383. clear_apertures_dict = dict()
  384. # everything will be stored in the '0' aperture since we are dealing with clear polygons not strokes
  385. clear_apertures_dict['0'] = dict()
  386. clear_apertures_dict['0']['size'] = 0.0
  387. clear_apertures_dict['0']['type'] = 'C'
  388. clear_apertures_dict['0']['geometry'] = []
  389. # on stroke color change we create a new apertures dictionary and store the old one in a storage from where
  390. # it will be transformed into Gerber object
  391. old_color = [None, None, None]
  392. # signal that we have clear geometry and the geometry will be added to a special layer_nr = 0
  393. flag_clear_geo = False
  394. line_nr = 0
  395. lines = pdf_content.splitlines()
  396. for pline in lines:
  397. if self.app.abort_flag:
  398. # graceful abort requested by the user
  399. raise FlatCAMApp.GracefulException
  400. line_nr += 1
  401. log.debug("line %d: %s" % (line_nr, pline))
  402. # COLOR DETECTION / OBJECT DETECTION
  403. match = self.stroke_color_re.search(pline)
  404. if match:
  405. color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
  406. log.debug(
  407. "ToolPDF.parse_pdf() --> STROKE Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
  408. (line_nr, color[0], color[1], color[2]))
  409. if color[0] == old_color[0] and color[1] == old_color[1] and color[2] == old_color[2]:
  410. # same color, do nothing
  411. continue
  412. else:
  413. if apertures_dict:
  414. object_dict[layer_nr] = deepcopy(apertures_dict)
  415. apertures_dict.clear()
  416. layer_nr += 1
  417. object_dict[layer_nr] = dict()
  418. old_color = copy(color)
  419. # we make sure that the following geometry is added to the right storage
  420. flag_clear_geo = False
  421. continue
  422. # CLEAR GEOMETRY detection
  423. match = self.fill_color_re.search(pline)
  424. if match:
  425. fill_color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
  426. log.debug(
  427. "ToolPDF.parse_pdf() --> FILL Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
  428. (line_nr, fill_color[0], fill_color[1], fill_color[2]))
  429. # if the color is white we are seeing 'clear_geometry' that can't be seen. It may be that those
  430. # geometries are actually holes from which we can make an Excellon file
  431. if fill_color[0] == 1 and fill_color[1] == 1 and fill_color[2] == 1:
  432. flag_clear_geo = True
  433. else:
  434. flag_clear_geo = False
  435. continue
  436. # TRANSFORMATIONS DETECTION #
  437. # Detect combined transformation.
  438. match = self.combined_transform_re.search(pline)
  439. if match:
  440. # detect save graphic stack event
  441. # sometimes they combine save_to_graphics_stack with the transformation on the same line
  442. if match.group(1) == 'q':
  443. log.debug(
  444. "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
  445. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  446. self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
  447. self.gs['line_width'].append(deepcopy(size))
  448. # transformation = TRANSLATION (OFFSET)
  449. if (float(match.group(3)) == 0 and float(match.group(4)) == 0) and \
  450. (float(match.group(6)) != 0 or float(match.group(7)) != 0):
  451. log.debug(
  452. "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
  453. offset_geo[0] += float(match.group(6))
  454. offset_geo[1] += float(match.group(7))
  455. # log.debug("Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
  456. # transformation = SCALING
  457. if float(match.group(2)) != 1 and float(match.group(5)) != 1:
  458. log.debug(
  459. "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
  460. scale_geo[0] *= float(match.group(2))
  461. scale_geo[1] *= float(match.group(5))
  462. # log.debug("Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
  463. continue
  464. # detect save graphic stack event
  465. match = self.save_gs_re.search(pline)
  466. if match:
  467. log.debug(
  468. "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
  469. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  470. self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
  471. self.gs['line_width'].append(deepcopy(size))
  472. # detect restore from graphic stack event
  473. match = self.restore_gs_re.search(pline)
  474. if match:
  475. try:
  476. restored_transform = self.gs['transform'].pop(-1)
  477. offset_geo = restored_transform[0]
  478. scale_geo = restored_transform[1]
  479. except IndexError:
  480. # nothing to remove
  481. log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
  482. pass
  483. try:
  484. size = self.gs['line_width'].pop(-1)
  485. except IndexError:
  486. log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
  487. # nothing to remove
  488. pass
  489. log.debug(
  490. "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> "
  491. "restored_offset=[%f, %f] ||| restored_scale=[%f, %f]" %
  492. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  493. # log.debug("Restored Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
  494. # log.debug("Restored Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
  495. # PATH CONSTRUCTION #
  496. # Start SUBPATH
  497. match = self.start_subpath_re.search(pline)
  498. if match:
  499. # we just started a subpath so we mark it as not closed yet
  500. close_subpath = False
  501. # init subpaths
  502. subpath['lines'] = []
  503. subpath['bezier'] = []
  504. subpath['rectangle'] = []
  505. # detect start point to move to
  506. x = float(match.group(1)) + offset_geo[0]
  507. y = float(match.group(2)) + offset_geo[1]
  508. pt = (x * self.point_to_unit_factor * scale_geo[0],
  509. y * self.point_to_unit_factor * scale_geo[1])
  510. start_point = pt
  511. # add the start point to subpaths
  512. subpath['lines'].append(start_point)
  513. # subpath['bezier'].append(start_point)
  514. # subpath['rectangle'].append(start_point)
  515. current_point = start_point
  516. continue
  517. # Draw Line
  518. match = self.draw_line_re.search(pline)
  519. if match:
  520. current_subpath = 'lines'
  521. x = float(match.group(1)) + offset_geo[0]
  522. y = float(match.group(2)) + offset_geo[1]
  523. pt = (x * self.point_to_unit_factor * scale_geo[0],
  524. y * self.point_to_unit_factor * scale_geo[1])
  525. subpath['lines'].append(pt)
  526. current_point = pt
  527. continue
  528. # Draw Bezier 'c'
  529. match = self.draw_arc_3pt_re.search(pline)
  530. if match:
  531. current_subpath = 'bezier'
  532. start = current_point
  533. x = float(match.group(1)) + offset_geo[0]
  534. y = float(match.group(2)) + offset_geo[1]
  535. c1 = (x * self.point_to_unit_factor * scale_geo[0],
  536. y * self.point_to_unit_factor * scale_geo[1])
  537. x = float(match.group(3)) + offset_geo[0]
  538. y = float(match.group(4)) + offset_geo[1]
  539. c2 = (x * self.point_to_unit_factor * scale_geo[0],
  540. y * self.point_to_unit_factor * scale_geo[1])
  541. x = float(match.group(5)) + offset_geo[0]
  542. y = float(match.group(6)) + offset_geo[1]
  543. stop = (x * self.point_to_unit_factor * scale_geo[0],
  544. y * self.point_to_unit_factor * scale_geo[1])
  545. subpath['bezier'].append([start, c1, c2, stop])
  546. current_point = stop
  547. continue
  548. # Draw Bezier 'v'
  549. match = self.draw_arc_2pt_c1start_re.search(pline)
  550. if match:
  551. current_subpath = 'bezier'
  552. start = current_point
  553. x = float(match.group(1)) + offset_geo[0]
  554. y = float(match.group(2)) + offset_geo[1]
  555. c2 = (x * self.point_to_unit_factor * scale_geo[0],
  556. y * self.point_to_unit_factor * scale_geo[1])
  557. x = float(match.group(3)) + offset_geo[0]
  558. y = float(match.group(4)) + offset_geo[1]
  559. stop = (x * self.point_to_unit_factor * scale_geo[0],
  560. y * self.point_to_unit_factor * scale_geo[1])
  561. subpath['bezier'].append([start, start, c2, stop])
  562. current_point = stop
  563. continue
  564. # Draw Bezier 'y'
  565. match = self.draw_arc_2pt_c2stop_re.search(pline)
  566. if match:
  567. start = current_point
  568. x = float(match.group(1)) + offset_geo[0]
  569. y = float(match.group(2)) + offset_geo[1]
  570. c1 = (x * self.point_to_unit_factor * scale_geo[0],
  571. y * self.point_to_unit_factor * scale_geo[1])
  572. x = float(match.group(3)) + offset_geo[0]
  573. y = float(match.group(4)) + offset_geo[1]
  574. stop = (x * self.point_to_unit_factor * scale_geo[0],
  575. y * self.point_to_unit_factor * scale_geo[1])
  576. subpath['bezier'].append([start, c1, stop, stop])
  577. current_point = stop
  578. continue
  579. # Draw Rectangle 're'
  580. match = self.rect_re.search(pline)
  581. if match:
  582. current_subpath = 'rectangle'
  583. x = (float(match.group(1)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0]
  584. y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1]
  585. width = (float(match.group(3)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0]
  586. height = (float(match.group(4)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1]
  587. pt1 = (x, y)
  588. pt2 = (x+width, y)
  589. pt3 = (x+width, y+height)
  590. pt4 = (x, y+height)
  591. subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1]
  592. current_point = pt1
  593. continue
  594. # Detect clipping path set
  595. # ignore this and delete the current subpath
  596. match = self.clip_path_re.search(pline)
  597. if match:
  598. subpath['lines'] = []
  599. subpath['bezier'] = []
  600. subpath['rectangle'] = []
  601. # it means that we've already added the subpath to path and we need to delete it
  602. # clipping path is usually either rectangle or lines
  603. if close_subpath is True:
  604. close_subpath = False
  605. if current_subpath == 'lines':
  606. path['lines'].pop(-1)
  607. if current_subpath == 'rectangle':
  608. path['rectangle'].pop(-1)
  609. continue
  610. # Close SUBPATH
  611. match = self.end_subpath_re.search(pline)
  612. if match:
  613. close_subpath = True
  614. if current_subpath == 'lines':
  615. subpath['lines'].append(start_point)
  616. # since we are closing the subpath add it to the path, a path may have chained subpaths
  617. path['lines'].append(copy(subpath['lines']))
  618. subpath['lines'] = []
  619. elif current_subpath == 'bezier':
  620. # subpath['bezier'].append(start_point)
  621. # since we are closing the subpath add it to the path, a path may have chained subpaths
  622. path['bezier'].append(copy(subpath['bezier']))
  623. subpath['bezier'] = []
  624. elif current_subpath == 'rectangle':
  625. # subpath['rectangle'].append(start_point)
  626. # since we are closing the subpath add it to the path, a path may have chained subpaths
  627. path['rectangle'].append(copy(subpath['rectangle']))
  628. subpath['rectangle'] = []
  629. continue
  630. # PATH PAINTING #
  631. # Detect Stroke width / aperture
  632. match = self.strokewidth_re.search(pline)
  633. if match:
  634. size = float(match.group(1))
  635. continue
  636. # Detect No_Op command, ignore the current subpath
  637. match = self.no_op_re.search(pline)
  638. if match:
  639. subpath['lines'] = []
  640. subpath['bezier'] = []
  641. subpath['rectangle'] = []
  642. continue
  643. # Stroke the path
  644. match = self.stroke_path__re.search(pline)
  645. if match:
  646. # scale the size here; some PDF printers apply transformation after the size is declared
  647. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  648. path_geo = list()
  649. if current_subpath == 'lines':
  650. if path['lines']:
  651. for subp in path['lines']:
  652. geo = copy(subp)
  653. try:
  654. geo = LineString(geo).buffer((float(applied_size) / 2),
  655. resolution=self.step_per_circles)
  656. path_geo.append(geo)
  657. except ValueError:
  658. pass
  659. # the path was painted therefore initialize it
  660. path['lines'] = []
  661. else:
  662. geo = copy(subpath['lines'])
  663. try:
  664. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  665. path_geo.append(geo)
  666. except ValueError:
  667. pass
  668. subpath['lines'] = []
  669. if current_subpath == 'bezier':
  670. if path['bezier']:
  671. for subp in path['bezier']:
  672. geo = []
  673. for b in subp:
  674. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  675. try:
  676. geo = LineString(geo).buffer((float(applied_size) / 2),
  677. resolution=self.step_per_circles)
  678. path_geo.append(geo)
  679. except ValueError:
  680. pass
  681. # the path was painted therefore initialize it
  682. path['bezier'] = []
  683. else:
  684. geo = []
  685. for b in subpath['bezier']:
  686. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  687. try:
  688. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  689. path_geo.append(geo)
  690. except ValueError:
  691. pass
  692. subpath['bezier'] = []
  693. if current_subpath == 'rectangle':
  694. if path['rectangle']:
  695. for subp in path['rectangle']:
  696. geo = copy(subp)
  697. try:
  698. geo = LineString(geo).buffer((float(applied_size) / 2),
  699. resolution=self.step_per_circles)
  700. path_geo.append(geo)
  701. except ValueError:
  702. pass
  703. # the path was painted therefore initialize it
  704. path['rectangle'] = []
  705. else:
  706. geo = copy(subpath['rectangle'])
  707. try:
  708. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  709. path_geo.append(geo)
  710. except ValueError:
  711. pass
  712. subpath['rectangle'] = []
  713. # store the found geometry
  714. found_aperture = None
  715. if apertures_dict:
  716. for apid in apertures_dict:
  717. # if we already have an aperture with the current size (rounded to 5 decimals)
  718. if apertures_dict[apid]['size'] == round(applied_size, 5):
  719. found_aperture = apid
  720. break
  721. if found_aperture:
  722. for pdf_geo in path_geo:
  723. if isinstance(pdf_geo, MultiPolygon):
  724. for poly in pdf_geo:
  725. new_el = dict()
  726. new_el['solid'] = poly
  727. new_el['follow'] = poly.exterior
  728. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  729. else:
  730. new_el = dict()
  731. new_el['solid'] = pdf_geo
  732. new_el['follow'] = pdf_geo.exterior
  733. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  734. found_aperture = None
  735. else:
  736. if str(aperture) in apertures_dict.keys():
  737. aperture += 1
  738. apertures_dict[str(aperture)] = {}
  739. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  740. apertures_dict[str(aperture)]['type'] = 'C'
  741. apertures_dict[str(aperture)]['geometry'] = []
  742. for pdf_geo in path_geo:
  743. if isinstance(pdf_geo, MultiPolygon):
  744. for poly in pdf_geo:
  745. new_el = dict()
  746. new_el['solid'] = poly
  747. new_el['follow'] = poly.exterior
  748. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  749. else:
  750. new_el = dict()
  751. new_el['solid'] = pdf_geo
  752. new_el['follow'] = pdf_geo.exterior
  753. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  754. else:
  755. apertures_dict[str(aperture)] = {}
  756. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  757. apertures_dict[str(aperture)]['type'] = 'C'
  758. apertures_dict[str(aperture)]['geometry'] = []
  759. for pdf_geo in path_geo:
  760. if isinstance(pdf_geo, MultiPolygon):
  761. for poly in pdf_geo:
  762. new_el = dict()
  763. new_el['solid'] = poly
  764. new_el['follow'] = poly.exterior
  765. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  766. else:
  767. new_el = dict()
  768. new_el['solid'] = pdf_geo
  769. new_el['follow'] = pdf_geo.exterior
  770. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  771. continue
  772. # Fill the path
  773. match = self.fill_path_re.search(pline)
  774. if match:
  775. # scale the size here; some PDF printers apply transformation after the size is declared
  776. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  777. path_geo = list()
  778. if current_subpath == 'lines':
  779. if path['lines']:
  780. for subp in path['lines']:
  781. geo = copy(subp)
  782. # close the subpath if it was not closed already
  783. if close_subpath is False:
  784. geo.append(geo[0])
  785. try:
  786. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  787. path_geo.append(geo_el)
  788. except ValueError:
  789. pass
  790. # the path was painted therefore initialize it
  791. path['lines'] = []
  792. else:
  793. geo = copy(subpath['lines'])
  794. # close the subpath if it was not closed already
  795. if close_subpath is False:
  796. geo.append(start_point)
  797. try:
  798. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  799. path_geo.append(geo_el)
  800. except ValueError:
  801. pass
  802. subpath['lines'] = []
  803. if current_subpath == 'bezier':
  804. geo = []
  805. if path['bezier']:
  806. for subp in path['bezier']:
  807. for b in subp:
  808. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  809. # close the subpath if it was not closed already
  810. if close_subpath is False:
  811. geo.append(geo[0])
  812. try:
  813. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  814. path_geo.append(geo_el)
  815. except ValueError:
  816. pass
  817. # the path was painted therefore initialize it
  818. path['bezier'] = []
  819. else:
  820. for b in subpath['bezier']:
  821. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  822. if close_subpath is False:
  823. geo.append(start_point)
  824. try:
  825. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  826. path_geo.append(geo_el)
  827. except ValueError:
  828. pass
  829. subpath['bezier'] = []
  830. if current_subpath == 'rectangle':
  831. if path['rectangle']:
  832. for subp in path['rectangle']:
  833. geo = copy(subp)
  834. # # close the subpath if it was not closed already
  835. # if close_subpath is False and start_point is not None:
  836. # geo.append(start_point)
  837. try:
  838. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  839. path_geo.append(geo_el)
  840. except ValueError:
  841. pass
  842. # the path was painted therefore initialize it
  843. path['rectangle'] = []
  844. else:
  845. geo = copy(subpath['rectangle'])
  846. # # close the subpath if it was not closed already
  847. # if close_subpath is False and start_point is not None:
  848. # geo.append(start_point)
  849. try:
  850. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  851. path_geo.append(geo_el)
  852. except ValueError:
  853. pass
  854. subpath['rectangle'] = []
  855. # we finished painting and also closed the path if it was the case
  856. close_subpath = True
  857. # in case that a color change to white (transparent) occurred
  858. if flag_clear_geo is True:
  859. # if there was a fill color change we look for circular geometries from which we can make
  860. # drill holes for the Excellon file
  861. if current_subpath == 'bezier':
  862. # if there are geometries in the list
  863. if path_geo:
  864. try:
  865. for g in path_geo:
  866. new_el = dict()
  867. new_el['clear'] = g
  868. clear_apertures_dict['0']['geometry'].append(new_el)
  869. except TypeError:
  870. new_el = dict()
  871. new_el['clear'] = path_geo
  872. clear_apertures_dict['0']['geometry'].append(new_el)
  873. # now that we finished searching for drill holes (this is not very precise because holes in the
  874. # polygon pours may appear as drill too, but .. hey you can't have it all ...) we add
  875. # clear_geometry
  876. try:
  877. for pdf_geo in path_geo:
  878. if isinstance(pdf_geo, MultiPolygon):
  879. for poly in pdf_geo:
  880. new_el = dict()
  881. new_el['clear'] = poly
  882. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  883. else:
  884. new_el = dict()
  885. new_el['clear'] = pdf_geo
  886. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  887. except KeyError:
  888. # in case there is no stroke width yet therefore no aperture
  889. apertures_dict['0'] = {}
  890. apertures_dict['0']['size'] = applied_size
  891. apertures_dict['0']['type'] = 'C'
  892. apertures_dict['0']['geometry'] = []
  893. for pdf_geo in path_geo:
  894. if isinstance(pdf_geo, MultiPolygon):
  895. for poly in pdf_geo:
  896. new_el = dict()
  897. new_el['clear'] = poly
  898. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  899. else:
  900. new_el = dict()
  901. new_el['clear'] = pdf_geo
  902. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  903. else:
  904. # else, add the geometry as usual
  905. try:
  906. for pdf_geo in path_geo:
  907. if isinstance(pdf_geo, MultiPolygon):
  908. for poly in pdf_geo:
  909. new_el = dict()
  910. new_el['solid'] = poly
  911. new_el['follow'] = poly.exterior
  912. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  913. else:
  914. new_el = dict()
  915. new_el['solid'] = pdf_geo
  916. new_el['follow'] = pdf_geo.exterior
  917. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  918. except KeyError:
  919. # in case there is no stroke width yet therefore no aperture
  920. apertures_dict['0'] = {}
  921. apertures_dict['0']['size'] = applied_size
  922. apertures_dict['0']['type'] = 'C'
  923. apertures_dict['0']['geometry'] = []
  924. for pdf_geo in path_geo:
  925. if isinstance(pdf_geo, MultiPolygon):
  926. for poly in pdf_geo:
  927. new_el = dict()
  928. new_el['solid'] = poly
  929. new_el['follow'] = poly.exterior
  930. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  931. else:
  932. new_el = dict()
  933. new_el['solid'] = pdf_geo
  934. new_el['follow'] = pdf_geo.exterior
  935. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  936. continue
  937. # Fill and Stroke the path
  938. match = self.fill_stroke_path_re.search(pline)
  939. if match:
  940. # scale the size here; some PDF printers apply transformation after the size is declared
  941. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  942. path_geo = list()
  943. fill_geo = list()
  944. if current_subpath == 'lines':
  945. if path['lines']:
  946. # fill
  947. for subp in path['lines']:
  948. geo = copy(subp)
  949. # close the subpath if it was not closed already
  950. if close_subpath is False:
  951. geo.append(geo[0])
  952. try:
  953. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  954. fill_geo.append(geo_el)
  955. except ValueError:
  956. pass
  957. # stroke
  958. for subp in path['lines']:
  959. geo = copy(subp)
  960. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  961. path_geo.append(geo)
  962. # the path was painted therefore initialize it
  963. path['lines'] = []
  964. else:
  965. # fill
  966. geo = copy(subpath['lines'])
  967. # close the subpath if it was not closed already
  968. if close_subpath is False:
  969. geo.append(start_point)
  970. try:
  971. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  972. fill_geo.append(geo_el)
  973. except ValueError:
  974. pass
  975. # stroke
  976. geo = copy(subpath['lines'])
  977. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  978. path_geo.append(geo)
  979. subpath['lines'] = []
  980. subpath['lines'] = []
  981. if current_subpath == 'bezier':
  982. geo = []
  983. if path['bezier']:
  984. # fill
  985. for subp in path['bezier']:
  986. for b in subp:
  987. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  988. # close the subpath if it was not closed already
  989. if close_subpath is False:
  990. geo.append(geo[0])
  991. try:
  992. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  993. fill_geo.append(geo_el)
  994. except ValueError:
  995. pass
  996. # stroke
  997. for subp in path['bezier']:
  998. geo = []
  999. for b in subp:
  1000. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  1001. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1002. path_geo.append(geo)
  1003. # the path was painted therefore initialize it
  1004. path['bezier'] = []
  1005. else:
  1006. # fill
  1007. for b in subpath['bezier']:
  1008. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  1009. if close_subpath is False:
  1010. geo.append(start_point)
  1011. try:
  1012. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  1013. fill_geo.append(geo_el)
  1014. except ValueError:
  1015. pass
  1016. # stroke
  1017. geo = []
  1018. for b in subpath['bezier']:
  1019. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  1020. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1021. path_geo.append(geo)
  1022. subpath['bezier'] = []
  1023. if current_subpath == 'rectangle':
  1024. if path['rectangle']:
  1025. # fill
  1026. for subp in path['rectangle']:
  1027. geo = copy(subp)
  1028. # # close the subpath if it was not closed already
  1029. # if close_subpath is False:
  1030. # geo.append(geo[0])
  1031. try:
  1032. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  1033. fill_geo.append(geo_el)
  1034. except ValueError:
  1035. pass
  1036. # stroke
  1037. for subp in path['rectangle']:
  1038. geo = copy(subp)
  1039. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1040. path_geo.append(geo)
  1041. # the path was painted therefore initialize it
  1042. path['rectangle'] = []
  1043. else:
  1044. # fill
  1045. geo = copy(subpath['rectangle'])
  1046. # # close the subpath if it was not closed already
  1047. # if close_subpath is False:
  1048. # geo.append(start_point)
  1049. try:
  1050. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  1051. fill_geo.append(geo_el)
  1052. except ValueError:
  1053. pass
  1054. # stroke
  1055. geo = copy(subpath['rectangle'])
  1056. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1057. path_geo.append(geo)
  1058. subpath['rectangle'] = []
  1059. # we finished painting and also closed the path if it was the case
  1060. close_subpath = True
  1061. # store the found geometry for stroking the path
  1062. found_aperture = None
  1063. if apertures_dict:
  1064. for apid in apertures_dict:
  1065. # if we already have an aperture with the current size (rounded to 5 decimals)
  1066. if apertures_dict[apid]['size'] == round(applied_size, 5):
  1067. found_aperture = apid
  1068. break
  1069. if found_aperture:
  1070. for pdf_geo in path_geo:
  1071. if isinstance(pdf_geo, MultiPolygon):
  1072. for poly in pdf_geo:
  1073. new_el = dict()
  1074. new_el['solid'] = poly
  1075. new_el['follow'] = poly.exterior
  1076. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  1077. else:
  1078. new_el = dict()
  1079. new_el['solid'] = pdf_geo
  1080. new_el['follow'] = pdf_geo.exterior
  1081. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  1082. found_aperture = None
  1083. else:
  1084. if str(aperture) in apertures_dict.keys():
  1085. aperture += 1
  1086. apertures_dict[str(aperture)] = {}
  1087. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  1088. apertures_dict[str(aperture)]['type'] = 'C'
  1089. apertures_dict[str(aperture)]['geometry'] = []
  1090. for pdf_geo in path_geo:
  1091. if isinstance(pdf_geo, MultiPolygon):
  1092. for poly in pdf_geo:
  1093. new_el = dict()
  1094. new_el['solid'] = poly
  1095. new_el['follow'] = poly.exterior
  1096. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1097. else:
  1098. new_el = dict()
  1099. new_el['solid'] = pdf_geo
  1100. new_el['follow'] = pdf_geo.exterior
  1101. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1102. else:
  1103. apertures_dict[str(aperture)] = {}
  1104. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  1105. apertures_dict[str(aperture)]['type'] = 'C'
  1106. apertures_dict[str(aperture)]['geometry'] = []
  1107. for pdf_geo in path_geo:
  1108. if isinstance(pdf_geo, MultiPolygon):
  1109. for poly in pdf_geo:
  1110. new_el = dict()
  1111. new_el['solid'] = poly
  1112. new_el['follow'] = poly.exterior
  1113. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1114. else:
  1115. new_el = dict()
  1116. new_el['solid'] = pdf_geo
  1117. new_el['follow'] = pdf_geo.exterior
  1118. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1119. # ############################################# ##
  1120. # store the found geometry for filling the path #
  1121. # ############################################# ##
  1122. # in case that a color change to white (transparent) occurred
  1123. if flag_clear_geo is True:
  1124. try:
  1125. for pdf_geo in path_geo:
  1126. if isinstance(pdf_geo, MultiPolygon):
  1127. for poly in fill_geo:
  1128. new_el = dict()
  1129. new_el['clear'] = poly
  1130. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1131. else:
  1132. new_el = dict()
  1133. new_el['clear'] = pdf_geo
  1134. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1135. except KeyError:
  1136. # in case there is no stroke width yet therefore no aperture
  1137. apertures_dict['0'] = {}
  1138. apertures_dict['0']['size'] = round(applied_size, 5)
  1139. apertures_dict['0']['type'] = 'C'
  1140. apertures_dict['0']['geometry'] = []
  1141. for pdf_geo in fill_geo:
  1142. if isinstance(pdf_geo, MultiPolygon):
  1143. for poly in pdf_geo:
  1144. new_el = dict()
  1145. new_el['clear'] = poly
  1146. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1147. else:
  1148. new_el = dict()
  1149. new_el['clear'] = pdf_geo
  1150. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1151. else:
  1152. try:
  1153. for pdf_geo in path_geo:
  1154. if isinstance(pdf_geo, MultiPolygon):
  1155. for poly in fill_geo:
  1156. new_el = dict()
  1157. new_el['solid'] = poly
  1158. new_el['follow'] = poly.exterior
  1159. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1160. else:
  1161. new_el = dict()
  1162. new_el['solid'] = pdf_geo
  1163. new_el['follow'] = pdf_geo.exterior
  1164. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1165. except KeyError:
  1166. # in case there is no stroke width yet therefore no aperture
  1167. apertures_dict['0'] = {}
  1168. apertures_dict['0']['size'] = round(applied_size, 5)
  1169. apertures_dict['0']['type'] = 'C'
  1170. apertures_dict['0']['geometry'] = []
  1171. for pdf_geo in fill_geo:
  1172. if isinstance(pdf_geo, MultiPolygon):
  1173. for poly in pdf_geo:
  1174. new_el = dict()
  1175. new_el['solid'] = poly
  1176. new_el['follow'] = poly.exterior
  1177. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1178. else:
  1179. new_el = dict()
  1180. new_el['solid'] = pdf_geo
  1181. new_el['follow'] = pdf_geo.exterior
  1182. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1183. continue
  1184. # tidy up. copy the current aperture dict to the object dict but only if it is not empty
  1185. if apertures_dict:
  1186. object_dict[layer_nr] = deepcopy(apertures_dict)
  1187. if clear_apertures_dict['0']['geometry']:
  1188. object_dict[0] = deepcopy(clear_apertures_dict)
  1189. # delete keys (layers) with empty values
  1190. empty_layers = []
  1191. for layer in object_dict:
  1192. if not object_dict[layer]:
  1193. empty_layers.append(layer)
  1194. for x in empty_layers:
  1195. if x in object_dict:
  1196. object_dict.pop(x)
  1197. if self.app.abort_flag:
  1198. # graceful abort requested by the user
  1199. raise FlatCAMApp.GracefulException
  1200. return object_dict
  1201. def bezier_to_points(self, start, c1, c2, stop):
  1202. """
  1203. # Equation Bezier, page 184 PDF 1.4 reference
  1204. # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  1205. # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0
  1206. # in the following equation:
  1207. # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2 + P3*t ** 3
  1208. # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides
  1209. # with the final point P3. Intermediate values of t generate intermediate points along the curve.
  1210. # The curve does not, in general, pass through the two control points P1 and P2
  1211. :return: A list of point coordinates tuples (x, y)
  1212. """
  1213. # here we store the geometric points
  1214. points = []
  1215. nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles))
  1216. for t in nr_points:
  1217. term_p0 = (1 - t) ** 3
  1218. term_p1 = 3 * t * (1 - t) ** 2
  1219. term_p2 = 3 * (1 - t) * t ** 2
  1220. term_p3 = t ** 3
  1221. x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3
  1222. y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3
  1223. points.append([x, y])
  1224. return points
  1225. # def bezier_to_circle(self, path):
  1226. # lst = []
  1227. # for el in range(len(path)):
  1228. # if type(path) is list:
  1229. # for coord in path[el]:
  1230. # lst.append(coord)
  1231. # else:
  1232. # lst.append(el)
  1233. #
  1234. # if lst:
  1235. # minx = min(lst, key=lambda t: t[0])[0]
  1236. # miny = min(lst, key=lambda t: t[1])[1]
  1237. # maxx = max(lst, key=lambda t: t[0])[0]
  1238. # maxy = max(lst, key=lambda t: t[1])[1]
  1239. # center = (maxx-minx, maxy-miny)
  1240. # radius = (maxx-minx) / 2
  1241. # return [center, radius]
  1242. #
  1243. # def circle_to_points(self, center, radius):
  1244. # geo = Point(center).buffer(radius, resolution=self.step_per_circles)
  1245. # return LineString(list(geo.exterior.coords))
  1246. #