ToolPDF.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  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 appTool import AppTool
  9. from appParsers.ParsePDF import PdfParser, grace
  10. from shapely.geometry import Point, MultiPolygon
  11. from shapely.ops import unary_union
  12. from copy import deepcopy
  13. import zlib
  14. import re
  15. import time
  16. import logging
  17. import traceback
  18. import gettext
  19. import appTranslation as fcTranslate
  20. import builtins
  21. fcTranslate.apply_language('strings')
  22. if '_' not in builtins.__dict__:
  23. _ = gettext.gettext
  24. log = logging.getLogger('base')
  25. class ToolPDF(AppTool):
  26. """
  27. Parse a PDF file.
  28. Reference here: https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  29. Return a list of geometries
  30. """
  31. toolName = _("PDF Import Tool")
  32. def __init__(self, app):
  33. AppTool.__init__(self, app)
  34. self.app = app
  35. self.decimals = self.app.decimals
  36. self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
  37. self.pdf_decompressed = {}
  38. # key = file name and extension
  39. # value is a dict to store the parsed content of the PDF
  40. self.pdf_parsed = {}
  41. # QTimer for periodic check
  42. self.check_thread = QtCore.QTimer()
  43. # Every time a parser is started we add a promise; every time a parser finished we remove a promise
  44. # when empty we start the layer rendering
  45. self.parsing_promises = []
  46. self.parser = PdfParser(app=self.app)
  47. def run(self, toggle=True):
  48. self.app.defaults.report_usage("ToolPDF()")
  49. self.set_tool_ui()
  50. self.on_open_pdf_click()
  51. def install(self, icon=None, separator=None, **kwargs):
  52. AppTool.install(self, icon, separator, shortcut='Ctrl+Q', **kwargs)
  53. def set_tool_ui(self):
  54. pass
  55. def on_open_pdf_click(self):
  56. """
  57. File menu callback for opening an PDF file.
  58. :return: None
  59. """
  60. self.app.defaults.report_usage("ToolPDF.on_open_pdf_click()")
  61. self.app.log.debug("ToolPDF.on_open_pdf_click()")
  62. _filter_ = "Adobe PDF Files (*.pdf);;" \
  63. "All Files (*.*)"
  64. try:
  65. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
  66. directory=self.app.get_last_folder(),
  67. filter=_filter_)
  68. except TypeError:
  69. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
  70. if len(filenames) == 0:
  71. self.app.inform.emit('[WARNING_NOTCL] %s.' % _("Open PDF cancelled"))
  72. else:
  73. # start the parsing timer with a period of 1 second
  74. self.periodic_check(1000)
  75. for filename in filenames:
  76. if filename != '':
  77. self.app.worker_task.emit({'fcn': self.open_pdf,
  78. 'params': [filename]})
  79. def open_pdf(self, filename):
  80. short_name = filename.split('/')[-1].split('\\')[-1]
  81. self.parsing_promises.append(short_name)
  82. self.pdf_parsed[short_name] = {
  83. 'pdf': {},
  84. 'filename': filename
  85. }
  86. self.pdf_decompressed[short_name] = ''
  87. if self.app.abort_flag:
  88. # graceful abort requested by the user
  89. raise grace
  90. with self.app.proc_container.new(_("Parsing ...")):
  91. with open(filename, "rb") as f:
  92. pdf = f.read()
  93. stream_nr = 0
  94. for s in re.findall(self.stream_re, pdf):
  95. if self.app.abort_flag:
  96. # graceful abort requested by the user
  97. raise grace
  98. stream_nr += 1
  99. log.debug("PDF STREAM: %d\n" % stream_nr)
  100. s = s.strip(b'\r\n')
  101. try:
  102. self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n')
  103. except Exception as e:
  104. self.app.inform.emit('[ERROR_NOTCL] %s: %s\n%s' % (_("Failed to open"), str(filename), str(e)))
  105. log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
  106. return
  107. self.pdf_parsed[short_name]['pdf'] = self.parser.parse_pdf(pdf_content=self.pdf_decompressed[short_name])
  108. # we used it, now we delete it
  109. self.pdf_decompressed[short_name] = ''
  110. # removal from list is done in a multithreaded way therefore not always the removal can be done
  111. # try to remove until it's done
  112. try:
  113. while True:
  114. self.parsing_promises.remove(short_name)
  115. time.sleep(0.1)
  116. except Exception as e:
  117. log.debug("ToolPDF.open_pdf() --> %s" % str(e))
  118. self.app.inform.emit('[success] %s: %s' % (_("Opened"), str(filename)))
  119. def layer_rendering_as_excellon(self, filename, ap_dict, layer_nr):
  120. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  121. # store the points here until reconstitution:
  122. # keys are diameters and values are list of (x,y) coords
  123. points = {}
  124. def obj_init(exc_obj, app_obj):
  125. clear_geo = [geo_el['clear'] for geo_el in ap_dict['0']['geometry']]
  126. for geo in clear_geo:
  127. xmin, ymin, xmax, ymax = geo.bounds
  128. center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
  129. # for drill bits, even in INCH, it's enough 3 decimals
  130. correction_factor = 0.974
  131. dia = (xmax - xmin) * correction_factor
  132. dia = round(dia, 3)
  133. if dia in points:
  134. points[dia].append(center)
  135. else:
  136. points[dia] = [center]
  137. sorted_dia = sorted(points.keys())
  138. name_tool = 0
  139. for dia in sorted_dia:
  140. name_tool += 1
  141. tool = str(name_tool)
  142. exc_obj.tools[tool] = {
  143. 'tooldia': dia,
  144. 'drills': [],
  145. 'solid_geometry': []
  146. }
  147. # update the drill list
  148. for dia_points in points:
  149. if dia == dia_points:
  150. for pt in points[dia_points]:
  151. exc_obj.tools[tool]['drills'].append(Point(pt))
  152. break
  153. ret = exc_obj.create_geometry()
  154. if ret == 'fail':
  155. log.debug("Could not create geometry for Excellon object.")
  156. return "fail"
  157. for tool in exc_obj.tools:
  158. if exc_obj.tools[tool]['solid_geometry']:
  159. return
  160. app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), outname))
  161. return "fail"
  162. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  163. ret_val = self.app.app_obj.new_object("excellon", outname, obj_init, autoselected=False)
  164. if ret_val == 'fail':
  165. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.'))
  166. return
  167. # Register recent file
  168. self.app.file_opened.emit("excellon", filename)
  169. # GUI feedback
  170. self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
  171. def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
  172. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  173. def obj_init(grb_obj, app_obj):
  174. grb_obj.apertures = ap_dict
  175. poly_buff = []
  176. follow_buf = []
  177. for ap in grb_obj.apertures:
  178. for k in grb_obj.apertures[ap]:
  179. if k == 'geometry':
  180. for geo_el in ap_dict[ap][k]:
  181. if 'solid' in geo_el:
  182. poly_buff.append(geo_el['solid'])
  183. if 'follow' in geo_el:
  184. follow_buf.append(geo_el['follow'])
  185. poly_buff = unary_union(poly_buff)
  186. if '0' in grb_obj.apertures:
  187. global_clear_geo = []
  188. if 'geometry' in grb_obj.apertures['0']:
  189. for geo_el in ap_dict['0']['geometry']:
  190. if 'clear' in geo_el:
  191. global_clear_geo.append(geo_el['clear'])
  192. if global_clear_geo:
  193. solid = []
  194. for apid in grb_obj.apertures:
  195. if 'geometry' in grb_obj.apertures[apid]:
  196. for elem in grb_obj.apertures[apid]['geometry']:
  197. if 'solid' in elem:
  198. solid_geo = deepcopy(elem['solid'])
  199. for clear_geo in global_clear_geo:
  200. # Make sure that the clear_geo is within the solid_geo otherwise we loose
  201. # the solid_geometry. We want for clear_geometry just to cut into solid_geometry
  202. # not to delete it
  203. if clear_geo.within(solid_geo):
  204. solid_geo = solid_geo.difference(clear_geo)
  205. if solid_geo.is_empty:
  206. solid_geo = elem['solid']
  207. try:
  208. for poly in solid_geo:
  209. solid.append(poly)
  210. except TypeError:
  211. solid.append(solid_geo)
  212. poly_buff = deepcopy(MultiPolygon(solid))
  213. follow_buf = unary_union(follow_buf)
  214. try:
  215. poly_buff = poly_buff.buffer(0.0000001)
  216. except ValueError:
  217. pass
  218. try:
  219. poly_buff = poly_buff.buffer(-0.0000001)
  220. except ValueError:
  221. pass
  222. grb_obj.solid_geometry = deepcopy(poly_buff)
  223. grb_obj.follow_geometry = deepcopy(follow_buf)
  224. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  225. ret = self.app.app_obj.new_object('gerber', outname, obj_init, autoselected=False)
  226. if ret == 'fail':
  227. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.'))
  228. return
  229. # Register recent file
  230. self.app.file_opened.emit('gerber', filename)
  231. # GUI feedback
  232. self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
  233. def periodic_check(self, check_period):
  234. """
  235. This function starts an QTimer and it will periodically check if parsing was done
  236. :param check_period: time at which to check periodically if all plots finished to be plotted
  237. :return:
  238. """
  239. # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
  240. # self.plot_thread.start()
  241. log.debug("ToolPDF --> Periodic Check started.")
  242. try:
  243. self.check_thread.stop()
  244. except TypeError:
  245. pass
  246. self.check_thread.setInterval(check_period)
  247. try:
  248. self.check_thread.timeout.disconnect(self.periodic_check_handler)
  249. except (TypeError, AttributeError):
  250. pass
  251. self.check_thread.timeout.connect(self.periodic_check_handler)
  252. self.check_thread.start(QtCore.QThread.HighPriority)
  253. def periodic_check_handler(self):
  254. """
  255. If the parsing worker finished then start multithreaded rendering
  256. :return:
  257. """
  258. # log.debug("checking parsing --> %s" % str(self.parsing_promises))
  259. try:
  260. if not self.parsing_promises:
  261. self.check_thread.stop()
  262. log.debug("PDF --> start rendering")
  263. # parsing finished start the layer rendering
  264. if self.pdf_parsed:
  265. obj_to_delete = []
  266. for object_name in self.pdf_parsed:
  267. if self.app.abort_flag:
  268. # graceful abort requested by the user
  269. raise grace
  270. filename = deepcopy(self.pdf_parsed[object_name]['filename'])
  271. pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
  272. obj_to_delete.append(object_name)
  273. for k in pdf_content:
  274. if self.app.abort_flag:
  275. # graceful abort requested by the user
  276. raise grace
  277. ap_dict = pdf_content[k]
  278. print(k, ap_dict)
  279. if ap_dict:
  280. layer_nr = k
  281. if k == 0:
  282. self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
  283. 'params': [filename, ap_dict, layer_nr]})
  284. else:
  285. self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
  286. 'params': [filename, ap_dict, layer_nr]})
  287. # delete the object already processed so it will not be processed again for other objects
  288. # that were opened at the same time; like in drag & drop on appGUI
  289. for obj_name in obj_to_delete:
  290. if obj_name in self.pdf_parsed:
  291. self.pdf_parsed.pop(obj_name)
  292. log.debug("ToolPDF --> Periodic check finished.")
  293. except Exception:
  294. traceback.print_exc()