ToolPDF.py 66 KB

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