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 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. self.pdf_parsed[short_name]['pdf'] = {}
  84. self.pdf_parsed[short_name]['filename'] = filename
  85. self.pdf_decompressed[short_name] = ''
  86. if self.app.abort_flag:
  87. # graceful abort requested by the user
  88. raise grace
  89. with self.app.proc_container.new(_("Parsing PDF file ...")):
  90. with open(filename, "rb") as f:
  91. pdf = f.read()
  92. stream_nr = 0
  93. for s in re.findall(self.stream_re, pdf):
  94. if self.app.abort_flag:
  95. # graceful abort requested by the user
  96. raise grace
  97. stream_nr += 1
  98. log.debug("PDF STREAM: %d\n" % stream_nr)
  99. s = s.strip(b'\r\n')
  100. try:
  101. self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n')
  102. except Exception as e:
  103. self.app.inform.emit('[ERROR_NOTCL] %s: %s\n%s' % (_("Failed to open"), str(filename), str(e)))
  104. log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
  105. return
  106. self.pdf_parsed[short_name]['pdf'] = self.parser.parse_pdf(pdf_content=self.pdf_decompressed[short_name])
  107. # we used it, now we delete it
  108. self.pdf_decompressed[short_name] = ''
  109. # removal from list is done in a multithreaded way therefore not always the removal can be done
  110. # try to remove until it's done
  111. try:
  112. while True:
  113. self.parsing_promises.remove(short_name)
  114. time.sleep(0.1)
  115. except Exception as e:
  116. log.debug("ToolPDF.open_pdf() --> %s" % str(e))
  117. self.app.inform.emit('[success] %s: %s' % (_("Opened"), str(filename)))
  118. def layer_rendering_as_excellon(self, filename, ap_dict, layer_nr):
  119. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  120. # store the points here until reconstitution:
  121. # keys are diameters and values are list of (x,y) coords
  122. points = {}
  123. def obj_init(exc_obj, app_obj):
  124. clear_geo = [geo_el['clear'] for geo_el in ap_dict['0']['geometry']]
  125. for geo in clear_geo:
  126. xmin, ymin, xmax, ymax = geo.bounds
  127. center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
  128. # for drill bits, even in INCH, it's enough 3 decimals
  129. correction_factor = 0.974
  130. dia = (xmax - xmin) * correction_factor
  131. dia = round(dia, 3)
  132. if dia in points:
  133. points[dia].append(center)
  134. else:
  135. points[dia] = [center]
  136. sorted_dia = sorted(points.keys())
  137. name_tool = 0
  138. for dia in sorted_dia:
  139. name_tool += 1
  140. # create tools dictionary
  141. spec = {"C": dia, 'solid_geometry': []}
  142. exc_obj.tools[str(name_tool)] = spec
  143. # create drill list of dictionaries
  144. for dia_points in points:
  145. if dia == dia_points:
  146. for pt in points[dia_points]:
  147. exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
  148. break
  149. ret = exc_obj.create_geometry()
  150. if ret == 'fail':
  151. log.debug("Could not create geometry for Excellon object.")
  152. return "fail"
  153. for tool in exc_obj.tools:
  154. if exc_obj.tools[tool]['solid_geometry']:
  155. return
  156. app_obj.inform.emit('[ERROR_NOTCL] %s: %s' % (_("No geometry found in file"), outname))
  157. return "fail"
  158. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  159. ret_val = self.app.app_obj.new_object("excellon", outname, obj_init, autoselected=False)
  160. if ret_val == 'fail':
  161. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.'))
  162. return
  163. # Register recent file
  164. self.app.file_opened.emit("excellon", filename)
  165. # GUI feedback
  166. self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
  167. def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
  168. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  169. def obj_init(grb_obj, app_obj):
  170. grb_obj.apertures = ap_dict
  171. poly_buff = []
  172. follow_buf = []
  173. for ap in grb_obj.apertures:
  174. for k in grb_obj.apertures[ap]:
  175. if k == 'geometry':
  176. for geo_el in ap_dict[ap][k]:
  177. if 'solid' in geo_el:
  178. poly_buff.append(geo_el['solid'])
  179. if 'follow' in geo_el:
  180. follow_buf.append(geo_el['follow'])
  181. poly_buff = unary_union(poly_buff)
  182. if '0' in grb_obj.apertures:
  183. global_clear_geo = []
  184. if 'geometry' in grb_obj.apertures['0']:
  185. for geo_el in ap_dict['0']['geometry']:
  186. if 'clear' in geo_el:
  187. global_clear_geo.append(geo_el['clear'])
  188. if global_clear_geo:
  189. solid = []
  190. for apid in grb_obj.apertures:
  191. if 'geometry' in grb_obj.apertures[apid]:
  192. for elem in grb_obj.apertures[apid]['geometry']:
  193. if 'solid' in elem:
  194. solid_geo = deepcopy(elem['solid'])
  195. for clear_geo in global_clear_geo:
  196. # Make sure that the clear_geo is within the solid_geo otherwise we loose
  197. # the solid_geometry. We want for clear_geometry just to cut into solid_geometry
  198. # not to delete it
  199. if clear_geo.within(solid_geo):
  200. solid_geo = solid_geo.difference(clear_geo)
  201. if solid_geo.is_empty:
  202. solid_geo = elem['solid']
  203. try:
  204. for poly in solid_geo:
  205. solid.append(poly)
  206. except TypeError:
  207. solid.append(solid_geo)
  208. poly_buff = deepcopy(MultiPolygon(solid))
  209. follow_buf = unary_union(follow_buf)
  210. try:
  211. poly_buff = poly_buff.buffer(0.0000001)
  212. except ValueError:
  213. pass
  214. try:
  215. poly_buff = poly_buff.buffer(-0.0000001)
  216. except ValueError:
  217. pass
  218. grb_obj.solid_geometry = deepcopy(poly_buff)
  219. grb_obj.follow_geometry = deepcopy(follow_buf)
  220. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  221. ret = self.app.app_obj.new_object('gerber', outname, obj_init, autoselected=False)
  222. if ret == 'fail':
  223. self.app.inform.emit('[ERROR_NOTCL] %s' % _('Open PDF file failed.'))
  224. return
  225. # Register recent file
  226. self.app.file_opened.emit('gerber', filename)
  227. # GUI feedback
  228. self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
  229. def periodic_check(self, check_period):
  230. """
  231. This function starts an QTimer and it will periodically check if parsing was done
  232. :param check_period: time at which to check periodically if all plots finished to be plotted
  233. :return:
  234. """
  235. # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
  236. # self.plot_thread.start()
  237. log.debug("ToolPDF --> Periodic Check started.")
  238. try:
  239. self.check_thread.stop()
  240. except TypeError:
  241. pass
  242. self.check_thread.setInterval(check_period)
  243. try:
  244. self.check_thread.timeout.disconnect(self.periodic_check_handler)
  245. except (TypeError, AttributeError):
  246. pass
  247. self.check_thread.timeout.connect(self.periodic_check_handler)
  248. self.check_thread.start(QtCore.QThread.HighPriority)
  249. def periodic_check_handler(self):
  250. """
  251. If the parsing worker finished then start multithreaded rendering
  252. :return:
  253. """
  254. # log.debug("checking parsing --> %s" % str(self.parsing_promises))
  255. try:
  256. if not self.parsing_promises:
  257. self.check_thread.stop()
  258. log.debug("PDF --> start rendering")
  259. # parsing finished start the layer rendering
  260. if self.pdf_parsed:
  261. obj_to_delete = []
  262. for object_name in self.pdf_parsed:
  263. if self.app.abort_flag:
  264. # graceful abort requested by the user
  265. raise grace
  266. filename = deepcopy(self.pdf_parsed[object_name]['filename'])
  267. pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
  268. obj_to_delete.append(object_name)
  269. for k in pdf_content:
  270. if self.app.abort_flag:
  271. # graceful abort requested by the user
  272. raise grace
  273. ap_dict = pdf_content[k]
  274. print(k, ap_dict)
  275. if ap_dict:
  276. layer_nr = k
  277. if k == 0:
  278. self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
  279. 'params': [filename, ap_dict, layer_nr]})
  280. else:
  281. self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
  282. 'params': [filename, ap_dict, layer_nr]})
  283. # delete the object already processed so it will not be processed again for other objects
  284. # that were opened at the same time; like in drag & drop on appGUI
  285. for obj_name in obj_to_delete:
  286. if obj_name in self.pdf_parsed:
  287. self.pdf_parsed.pop(obj_name)
  288. log.debug("ToolPDF --> Periodic check finished.")
  289. except Exception:
  290. traceback.print_exc()