ToolPDF.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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 Common import GracefulException as grace
  10. from AppParsers.ParsePDF import PdfParser
  11. from shapely.geometry import Point, MultiPolygon
  12. from shapely.ops import unary_union
  13. from copy import deepcopy
  14. import zlib
  15. import re
  16. import time
  17. import logging
  18. import traceback
  19. import gettext
  20. import AppTranslation 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(AppTool):
  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. AppTool.__init__(self, app)
  35. self.app = app
  36. self.decimals = self.app.decimals
  37. self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
  38. self.pdf_decompressed = {}
  39. # key = file name and extension
  40. # value is a dict to store the parsed content of the PDF
  41. self.pdf_parsed = {}
  42. # QTimer for periodic check
  43. self.check_thread = QtCore.QTimer()
  44. # Every time a parser is started we add a promise; every time a parser finished we remove a promise
  45. # when empty we start the layer rendering
  46. self.parsing_promises = []
  47. self.parser = PdfParser(app=self.app)
  48. def run(self, toggle=True):
  49. self.app.defaults.report_usage("ToolPDF()")
  50. self.set_tool_ui()
  51. self.on_open_pdf_click()
  52. def install(self, icon=None, separator=None, **kwargs):
  53. AppTool.install(self, icon, separator, shortcut='Ctrl+Q', **kwargs)
  54. def set_tool_ui(self):
  55. pass
  56. def on_open_pdf_click(self):
  57. """
  58. File menu callback for opening an PDF file.
  59. :return: None
  60. """
  61. self.app.defaults.report_usage("ToolPDF.on_open_pdf_click()")
  62. self.app.log.debug("ToolPDF.on_open_pdf_click()")
  63. _filter_ = "Adobe PDF Files (*.pdf);;" \
  64. "All Files (*.*)"
  65. try:
  66. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
  67. directory=self.app.get_last_folder(),
  68. filter=_filter_)
  69. except TypeError:
  70. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
  71. if len(filenames) == 0:
  72. self.app.inform.emit('[WARNING_NOTCL] %s.' % _("Open PDF cancelled"))
  73. else:
  74. # start the parsing timer with a period of 1 second
  75. self.periodic_check(1000)
  76. for filename in filenames:
  77. if filename != '':
  78. self.app.worker_task.emit({'fcn': self.open_pdf,
  79. 'params': [filename]})
  80. def open_pdf(self, filename):
  81. short_name = filename.split('/')[-1].split('\\')[-1]
  82. self.parsing_promises.append(short_name)
  83. self.pdf_parsed[short_name] = {}
  84. self.pdf_parsed[short_name]['pdf'] = {}
  85. self.pdf_parsed[short_name]['filename'] = filename
  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 PDF file ...")):
  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. # create tools dictionary
  142. spec = {"C": dia, 'solid_geometry': []}
  143. exc_obj.tools[str(name_tool)] = spec
  144. # create drill list of dictionaries
  145. for dia_points in points:
  146. if dia == dia_points:
  147. for pt in points[dia_points]:
  148. exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
  149. break
  150. ret = exc_obj.create_geometry()
  151. if ret == 'fail':
  152. log.debug("Could not create geometry for Excellon object.")
  153. return "fail"
  154. for tool in exc_obj.tools:
  155. if exc_obj.tools[tool]['solid_geometry']:
  156. return
  157. app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), outname))
  158. return "fail"
  159. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  160. ret_val = self.app.app_obj.new_object("excellon", outname, obj_init, autoselected=False)
  161. if ret_val == 'fail':
  162. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.'))
  163. return
  164. # Register recent file
  165. self.app.file_opened.emit("excellon", filename)
  166. # GUI feedback
  167. self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
  168. def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
  169. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  170. def obj_init(grb_obj, app_obj):
  171. grb_obj.apertures = ap_dict
  172. poly_buff = []
  173. follow_buf = []
  174. for ap in grb_obj.apertures:
  175. for k in grb_obj.apertures[ap]:
  176. if k == 'geometry':
  177. for geo_el in ap_dict[ap][k]:
  178. if 'solid' in geo_el:
  179. poly_buff.append(geo_el['solid'])
  180. if 'follow' in geo_el:
  181. follow_buf.append(geo_el['follow'])
  182. poly_buff = unary_union(poly_buff)
  183. if '0' in grb_obj.apertures:
  184. global_clear_geo = []
  185. if 'geometry' in grb_obj.apertures['0']:
  186. for geo_el in ap_dict['0']['geometry']:
  187. if 'clear' in geo_el:
  188. global_clear_geo.append(geo_el['clear'])
  189. if global_clear_geo:
  190. solid = []
  191. for apid in grb_obj.apertures:
  192. if 'geometry' in grb_obj.apertures[apid]:
  193. for elem in grb_obj.apertures[apid]['geometry']:
  194. if 'solid' in elem:
  195. solid_geo = deepcopy(elem['solid'])
  196. for clear_geo in global_clear_geo:
  197. # Make sure that the clear_geo is within the solid_geo otherwise we loose
  198. # the solid_geometry. We want for clear_geometry just to cut into solid_geometry
  199. # not to delete it
  200. if clear_geo.within(solid_geo):
  201. solid_geo = solid_geo.difference(clear_geo)
  202. if solid_geo.is_empty:
  203. solid_geo = elem['solid']
  204. try:
  205. for poly in solid_geo:
  206. solid.append(poly)
  207. except TypeError:
  208. solid.append(solid_geo)
  209. poly_buff = deepcopy(MultiPolygon(solid))
  210. follow_buf = unary_union(follow_buf)
  211. try:
  212. poly_buff = poly_buff.buffer(0.0000001)
  213. except ValueError:
  214. pass
  215. try:
  216. poly_buff = poly_buff.buffer(-0.0000001)
  217. except ValueError:
  218. pass
  219. grb_obj.solid_geometry = deepcopy(poly_buff)
  220. grb_obj.follow_geometry = deepcopy(follow_buf)
  221. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  222. ret = self.app.app_obj.new_object('gerber', outname, obj_init, autoselected=False)
  223. if ret == 'fail':
  224. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.'))
  225. return
  226. # Register recent file
  227. self.app.file_opened.emit('gerber', filename)
  228. # GUI feedback
  229. self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
  230. def periodic_check(self, check_period):
  231. """
  232. This function starts an QTimer and it will periodically check if parsing was done
  233. :param check_period: time at which to check periodically if all plots finished to be plotted
  234. :return:
  235. """
  236. # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
  237. # self.plot_thread.start()
  238. log.debug("ToolPDF --> Periodic Check started.")
  239. try:
  240. self.check_thread.stop()
  241. except TypeError:
  242. pass
  243. self.check_thread.setInterval(check_period)
  244. try:
  245. self.check_thread.timeout.disconnect(self.periodic_check_handler)
  246. except (TypeError, AttributeError):
  247. pass
  248. self.check_thread.timeout.connect(self.periodic_check_handler)
  249. self.check_thread.start(QtCore.QThread.HighPriority)
  250. def periodic_check_handler(self):
  251. """
  252. If the parsing worker finished then start multithreaded rendering
  253. :return:
  254. """
  255. # log.debug("checking parsing --> %s" % str(self.parsing_promises))
  256. try:
  257. if not self.parsing_promises:
  258. self.check_thread.stop()
  259. log.debug("PDF --> start rendering")
  260. # parsing finished start the layer rendering
  261. if self.pdf_parsed:
  262. obj_to_delete = []
  263. for object_name in self.pdf_parsed:
  264. if self.app.abort_flag:
  265. # graceful abort requested by the user
  266. raise grace
  267. filename = deepcopy(self.pdf_parsed[object_name]['filename'])
  268. pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
  269. obj_to_delete.append(object_name)
  270. for k in pdf_content:
  271. if self.app.abort_flag:
  272. # graceful abort requested by the user
  273. raise grace
  274. ap_dict = pdf_content[k]
  275. print(k, ap_dict)
  276. if ap_dict:
  277. layer_nr = k
  278. if k == 0:
  279. self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
  280. 'params': [filename, ap_dict, layer_nr]})
  281. else:
  282. self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
  283. 'params': [filename, ap_dict, layer_nr]})
  284. # delete the object already processed so it will not be processed again for other objects
  285. # that were opened at the same time; like in drag & drop on AppGUI
  286. for obj_name in obj_to_delete:
  287. if obj_name in self.pdf_parsed:
  288. self.pdf_parsed.pop(obj_name)
  289. log.debug("ToolPDF --> Periodic check finished.")
  290. except Exception:
  291. traceback.print_exc()