ToolPDF.py 66 KB

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