ToolPDF.py 50 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrian Stanciu (c) #
  5. # Date: 4/23/2019 #
  6. # MIT Licence #
  7. ############################################################
  8. from FlatCAMTool import FlatCAMTool
  9. from shapely.geometry import Point, Polygon, LineString
  10. from shapely.ops import cascaded_union, unary_union
  11. from FlatCAMObj import *
  12. import math
  13. from copy import copy, deepcopy
  14. import numpy as np
  15. import zlib
  16. import re
  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] 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. with self.app.proc_container.new(_("Parsing PDF file ...")):
  147. with open(filename, "rb") as f:
  148. pdf = f.read()
  149. stream_nr = 0
  150. for s in re.findall(self.stream_re, pdf):
  151. stream_nr += 1
  152. log.debug(" PDF STREAM: %d\n" % stream_nr)
  153. s = s.strip(b'\r\n')
  154. try:
  155. self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n')
  156. except Exception as e:
  157. log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
  158. self.pdf_parsed[short_name]['pdf'] = self.parse_pdf(pdf_content=self.pdf_decompressed[short_name])
  159. # we used it, now we delete it
  160. self.pdf_decompressed[short_name] = ''
  161. # removal from list is done in a multithreaded way therefore not always the removal can be done
  162. try:
  163. self.parsing_promises.remove(short_name)
  164. except:
  165. pass
  166. self.app.inform.emit(_("[success] Opened: %s") % filename)
  167. def layer_rendering_as_excellon(self, filename, ap_dict, layer_nr):
  168. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  169. # store the points here until reconstitution:
  170. # keys are diameters and values are list of (x,y) coords
  171. points = {}
  172. def obj_init(exc_obj, app_obj):
  173. for geo in ap_dict['0']['solid_geometry']:
  174. xmin, ymin, xmax, ymax = geo.bounds
  175. center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
  176. # for drill bits, even in INCH, it's enough 3 decimals
  177. correction_factor = 0.974
  178. dia = (xmax - xmin) * correction_factor
  179. dia = round(dia, 3)
  180. if dia in points:
  181. points[dia].append(center)
  182. else:
  183. points[dia] = [center]
  184. sorted_dia = sorted(points.keys())
  185. name_tool = 0
  186. for dia in sorted_dia:
  187. name_tool += 1
  188. # create tools dictionary
  189. spec = {"C": dia}
  190. spec['solid_geometry'] = []
  191. exc_obj.tools[str(name_tool)] = spec
  192. # create drill list of dictionaries
  193. for dia_points in points:
  194. if dia == dia_points:
  195. for pt in points[dia_points]:
  196. exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
  197. break
  198. ret = exc_obj.create_geometry()
  199. if ret == 'fail':
  200. log.debug("Could not create geometry for Excellon object.")
  201. return "fail"
  202. for tool in exc_obj.tools:
  203. if exc_obj.tools[tool]['solid_geometry']:
  204. return
  205. app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % outname)
  206. return "fail"
  207. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  208. ret = self.app.new_object("excellon", outname, obj_init, autoselected=False)
  209. if ret == 'fail':
  210. self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
  211. return
  212. # Register recent file
  213. self.app.file_opened.emit("excellon", filename)
  214. # GUI feedback
  215. self.app.inform.emit(_("[success] Rendered: %s") % outname)
  216. def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
  217. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  218. def obj_init(grb_obj, app_obj):
  219. grb_obj.apertures = ap_dict
  220. poly_buff = []
  221. for ap in grb_obj.apertures:
  222. for k in grb_obj.apertures[ap]:
  223. if k == 'solid_geometry':
  224. poly_buff += ap_dict[ap][k]
  225. poly_buff = unary_union(poly_buff)
  226. try:
  227. poly_buff = poly_buff.buffer(0.0000001)
  228. except ValueError:
  229. pass
  230. try:
  231. poly_buff = poly_buff.buffer(-0.0000001)
  232. except ValueError:
  233. pass
  234. grb_obj.solid_geometry = deepcopy(poly_buff)
  235. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  236. ret = self.app.new_object('gerber', outname, obj_init, autoselected=False)
  237. if ret == 'fail':
  238. self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
  239. return
  240. # Register recent file
  241. self.app.file_opened.emit('gerber', filename)
  242. # GUI feedback
  243. self.app.inform.emit(_("[success] Rendered: %s") % outname)
  244. def periodic_check(self, check_period):
  245. """
  246. This function starts an QTimer and it will periodically check if parsing was done
  247. :param check_period: time at which to check periodically if all plots finished to be plotted
  248. :return:
  249. """
  250. # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
  251. # self.plot_thread.start()
  252. log.debug("ToolPDF --> Periodic Check started.")
  253. try:
  254. self.check_thread.stop()
  255. except:
  256. pass
  257. self.check_thread.setInterval(check_period)
  258. try:
  259. self.check_thread.timeout.disconnect(self.periodic_check_handler)
  260. except:
  261. pass
  262. self.check_thread.timeout.connect(self.periodic_check_handler)
  263. self.check_thread.start(QtCore.QThread.HighPriority)
  264. def periodic_check_handler(self):
  265. """
  266. If the parsing worker finished then start multithreaded rendering
  267. :return:
  268. """
  269. # log.debug("checking parsing --> %s" % str(self.parsing_promises))
  270. try:
  271. if not self.parsing_promises:
  272. self.check_thread.stop()
  273. # parsing finished start the layer rendering
  274. if self.pdf_parsed:
  275. obj_to_delete = []
  276. for object_name in self.pdf_parsed:
  277. filename = deepcopy(self.pdf_parsed[object_name]['filename'])
  278. pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
  279. obj_to_delete.append(object_name)
  280. for k in pdf_content:
  281. ap_dict = pdf_content[k]
  282. if ap_dict:
  283. layer_nr = k
  284. if k == 0:
  285. self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
  286. 'params': [filename, ap_dict, layer_nr]})
  287. else:
  288. self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
  289. 'params': [filename, ap_dict, layer_nr]})
  290. # delete the object already processed so it will not be processed again for other objects
  291. # that were opened at the same time; like in drag & drop on GUI
  292. for obj_name in obj_to_delete:
  293. if obj_name in self.pdf_parsed:
  294. self.pdf_parsed.pop(obj_name)
  295. log.debug("ToolPDF --> Periodic check finished.")
  296. except Exception:
  297. traceback.print_exc()
  298. def parse_pdf(self, pdf_content):
  299. path = dict()
  300. path['lines'] = [] # it's a list of lines subpaths
  301. path['bezier'] = [] # it's a list of bezier arcs subpaths
  302. path['rectangle'] = [] # it's a list of rectangle subpaths
  303. subpath = dict()
  304. subpath['lines'] = [] # it's a list of points
  305. subpath['bezier'] = [] # it's a list of sublists each like this [start, c1, c2, stop]
  306. subpath['rectangle'] = [] # it's a list of sublists of points
  307. # store the start point (when 'm' command is encountered)
  308. current_subpath = None
  309. # set True when 'h' command is encountered (close subpath)
  310. close_subpath = False
  311. start_point = None
  312. current_point = None
  313. size = 0
  314. # initial values for the transformations, in case they are not encountered in the PDF file
  315. offset_geo = [0, 0]
  316. scale_geo = [1, 1]
  317. # store the objects to be transformed into Gerbers
  318. object_dict = {}
  319. # will serve as key in the object_dict
  320. layer_nr = 1
  321. # create first object
  322. object_dict[layer_nr] = {}
  323. # store the apertures here
  324. apertures_dict = {}
  325. # initial aperture
  326. aperture = 10
  327. # store the apertures with clear geometry here
  328. # we are interested only in the circular geometry (drill holes) therefore we target only Bezier subpaths
  329. clear_apertures_dict = dict()
  330. # everything will be stored in the '0' aperture since we are dealing with clear polygons not strokes
  331. clear_apertures_dict['0'] = dict()
  332. clear_apertures_dict['0']['size'] = 0.0
  333. clear_apertures_dict['0']['type'] = 'C'
  334. clear_apertures_dict['0']['solid_geometry'] = []
  335. # on stroke color change we create a new apertures dictionary and store the old one in a storage from where
  336. # it will be transformed into Gerber object
  337. old_color = [None, None ,None]
  338. # signal that we have clear geometry and the geometry will be added to a special layer_nr = 0
  339. flag_clear_geo = False
  340. line_nr = 0
  341. lines = pdf_content.splitlines()
  342. for pline in lines:
  343. line_nr += 1
  344. # log.debug("line %d: %s" % (line_nr, pline))
  345. # COLOR DETECTION / OBJECT DETECTION
  346. match = self.stroke_color_re.search(pline)
  347. if match:
  348. color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
  349. log.debug(
  350. "ToolPDF.parse_pdf() --> STROKE Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
  351. (line_nr, color[0], color[1], color[2]))
  352. if color[0] == old_color[0] and color[1] == old_color[1] and color[2] == old_color[2]:
  353. # same color, do nothing
  354. continue
  355. else:
  356. if apertures_dict:
  357. object_dict[layer_nr] = deepcopy(apertures_dict)
  358. apertures_dict.clear()
  359. layer_nr += 1
  360. object_dict[layer_nr] = dict()
  361. old_color = copy(color)
  362. # we make sure that the following geometry is added to the right storage
  363. flag_clear_geo = False
  364. continue
  365. # CLEAR GEOMETRY detection
  366. match = self.fill_color_re.search(pline)
  367. if match:
  368. fill_color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
  369. log.debug(
  370. "ToolPDF.parse_pdf() --> FILL Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
  371. (line_nr, fill_color[0], fill_color[1], fill_color[2]))
  372. # if the color is white we are seeing 'clear_geometry' that can't be seen. It may be that those
  373. # geometries are actually holes from which we can make an Excellon file
  374. if fill_color[0] == 1 and fill_color[1] == 1 and fill_color[2] == 1:
  375. flag_clear_geo = True
  376. else:
  377. flag_clear_geo = False
  378. continue
  379. # TRANSFORMATIONS DETECTION #
  380. # Detect combined transformation.
  381. match = self.combined_transform_re.search(pline)
  382. if match:
  383. # detect save graphic stack event
  384. # sometimes they combine save_to_graphics_stack with the transformation on the same line
  385. if match.group(1) == 'q':
  386. log.debug(
  387. "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
  388. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  389. self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
  390. self.gs['line_width'].append(deepcopy(size))
  391. # transformation = TRANSLATION (OFFSET)
  392. if (float(match.group(3)) == 0 and float(match.group(4)) == 0) and \
  393. (float(match.group(6)) != 0 or float(match.group(7)) != 0):
  394. log.debug(
  395. "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
  396. offset_geo[0] += float(match.group(6))
  397. offset_geo[1] += float(match.group(7))
  398. # log.debug("Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
  399. # transformation = SCALING
  400. if float(match.group(2)) != 1 and float(match.group(5)) != 1:
  401. log.debug(
  402. "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
  403. scale_geo[0] *= float(match.group(2))
  404. scale_geo[1] *= float(match.group(5))
  405. # log.debug("Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
  406. continue
  407. # detect save graphic stack event
  408. match = self.save_gs_re.search(pline)
  409. if match:
  410. log.debug(
  411. "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
  412. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  413. self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
  414. self.gs['line_width'].append(deepcopy(size))
  415. # detect restore from graphic stack event
  416. match = self.restore_gs_re.search(pline)
  417. if match:
  418. log.debug(
  419. "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> %s" % (line_nr, pline))
  420. try:
  421. restored_transform = self.gs['transform'].pop(-1)
  422. offset_geo = restored_transform[0]
  423. scale_geo = restored_transform[1]
  424. except IndexError:
  425. # nothing to remove
  426. log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
  427. pass
  428. try:
  429. size = self.gs['line_width'].pop(-1)
  430. except IndexError:
  431. log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
  432. # nothing to remove
  433. pass
  434. # log.debug("Restored Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
  435. # log.debug("Restored Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
  436. # PATH CONSTRUCTION #
  437. # Start SUBPATH
  438. match = self.start_subpath_re.search(pline)
  439. if match:
  440. # we just started a subpath so we mark it as not closed yet
  441. close_subpath = False
  442. # init subpaths
  443. subpath['lines'] = []
  444. subpath['bezier'] = []
  445. subpath['rectangle'] = []
  446. # detect start point to move to
  447. x = float(match.group(1)) + offset_geo[0]
  448. y = float(match.group(2)) + offset_geo[1]
  449. pt = (x * self.point_to_unit_factor * scale_geo[0],
  450. y * self.point_to_unit_factor * scale_geo[1])
  451. start_point = pt
  452. # add the start point to subpaths
  453. subpath['lines'].append(start_point)
  454. # subpath['bezier'].append(start_point)
  455. # subpath['rectangle'].append(start_point)
  456. current_point = start_point
  457. continue
  458. # Draw Line
  459. match = self.draw_line_re.search(pline)
  460. if match:
  461. current_subpath = 'lines'
  462. x = float(match.group(1)) + offset_geo[0]
  463. y = float(match.group(2)) + offset_geo[1]
  464. pt = (x * self.point_to_unit_factor * scale_geo[0],
  465. y * self.point_to_unit_factor * scale_geo[1])
  466. subpath['lines'].append(pt)
  467. current_point = pt
  468. continue
  469. # Draw Bezier 'c'
  470. match = self.draw_arc_3pt_re.search(pline)
  471. if match:
  472. current_subpath = 'bezier'
  473. start = current_point
  474. x = float(match.group(1)) + offset_geo[0]
  475. y = float(match.group(2)) + offset_geo[1]
  476. c1 = (x * self.point_to_unit_factor * scale_geo[0],
  477. y * self.point_to_unit_factor * scale_geo[1])
  478. x = float(match.group(3)) + offset_geo[0]
  479. y = float(match.group(4)) + offset_geo[1]
  480. c2 = (x * self.point_to_unit_factor * scale_geo[0],
  481. y * self.point_to_unit_factor * scale_geo[1])
  482. x = float(match.group(5)) + offset_geo[0]
  483. y = float(match.group(6)) + offset_geo[1]
  484. stop = (x * self.point_to_unit_factor * scale_geo[0],
  485. y * self.point_to_unit_factor * scale_geo[1])
  486. subpath['bezier'].append([start, c1, c2, stop])
  487. current_point = stop
  488. continue
  489. # Draw Bezier 'v'
  490. match = self.draw_arc_2pt_c1start_re.search(pline)
  491. if match:
  492. current_subpath = 'bezier'
  493. start = current_point
  494. x = float(match.group(1)) + offset_geo[0]
  495. y = float(match.group(2)) + offset_geo[1]
  496. c2 = (x * self.point_to_unit_factor * scale_geo[0],
  497. y * self.point_to_unit_factor * scale_geo[1])
  498. x = float(match.group(3)) + offset_geo[0]
  499. y = float(match.group(4)) + offset_geo[1]
  500. stop = (x * self.point_to_unit_factor * scale_geo[0],
  501. y * self.point_to_unit_factor * scale_geo[1])
  502. subpath['bezier'].append([start, start, c2, stop])
  503. current_point = stop
  504. continue
  505. # Draw Bezier 'y'
  506. match = self.draw_arc_2pt_c2stop_re.search(pline)
  507. if match:
  508. start = current_point
  509. x = float(match.group(1)) + offset_geo[0]
  510. y = float(match.group(2)) + offset_geo[1]
  511. c1 = (x * self.point_to_unit_factor * scale_geo[0],
  512. y * self.point_to_unit_factor * scale_geo[1])
  513. x = float(match.group(3)) + offset_geo[0]
  514. y = float(match.group(4)) + offset_geo[1]
  515. stop = (x * self.point_to_unit_factor * scale_geo[0],
  516. y * self.point_to_unit_factor * scale_geo[1])
  517. subpath['bezier'].append([start, c1, stop, stop])
  518. current_point = stop
  519. continue
  520. # Draw Rectangle 're'
  521. match = self.rect_re.search(pline)
  522. if match:
  523. current_subpath = 'rectangle'
  524. x = (float(match.group(1)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0]
  525. y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1]
  526. width = (float(match.group(3)) + offset_geo[0]) * \
  527. self.point_to_unit_factor * scale_geo[0]
  528. height = (float(match.group(4)) + offset_geo[1]) * \
  529. self.point_to_unit_factor * scale_geo[1]
  530. pt1 = (x, y)
  531. pt2 = (x+width, y)
  532. pt3 = (x+width, y+height)
  533. pt4 = (x, y+height)
  534. subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1]
  535. current_point = pt1
  536. continue
  537. # Detect clipping path set
  538. # ignore this and delete the current subpath
  539. match = self.clip_path_re.search(pline)
  540. if match:
  541. subpath['lines'] = []
  542. subpath['bezier'] = []
  543. subpath['rectangle'] = []
  544. # it measns that we've already added the subpath to path and we need to delete it
  545. # clipping path is usually either rectangle or lines
  546. if close_subpath is True:
  547. close_subpath = False
  548. if current_subpath == 'lines':
  549. path['lines'].pop(-1)
  550. if current_subpath == 'rectangle':
  551. path['rectangle'].pop(-1)
  552. continue
  553. # Close SUBPATH
  554. match = self.end_subpath_re.search(pline)
  555. if match:
  556. close_subpath = True
  557. if current_subpath == 'lines':
  558. subpath['lines'].append(start_point)
  559. # since we are closing the subpath add it to the path, a path may have chained subpaths
  560. path['lines'].append(copy(subpath['lines']))
  561. subpath['lines'] = []
  562. elif current_subpath == 'bezier':
  563. # subpath['bezier'].append(start_point)
  564. # since we are closing the subpath add it to the path, a path may have chained subpaths
  565. path['bezier'].append(copy(subpath['bezier']))
  566. subpath['bezier'] = []
  567. elif current_subpath == 'rectangle':
  568. # subpath['rectangle'].append(start_point)
  569. # since we are closing the subpath add it to the path, a path may have chained subpaths
  570. path['rectangle'].append(copy(subpath['rectangle']))
  571. subpath['rectangle'] = []
  572. continue
  573. # PATH PAINTING #
  574. # Detect Stroke width / aperture
  575. match = self.strokewidth_re.search(pline)
  576. if match:
  577. size = float(match.group(1))
  578. continue
  579. # Detect No_Op command, ignore the current subpath
  580. match = self.no_op_re.search(pline)
  581. if match:
  582. subpath['lines'] = []
  583. subpath['bezier'] = []
  584. subpath['rectangle'] = []
  585. continue
  586. # Stroke the path
  587. match = self.stroke_path__re.search(pline)
  588. if match:
  589. # scale the size here; some PDF printers apply transformation after the size is declared
  590. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  591. path_geo = list()
  592. if current_subpath == 'lines':
  593. if path['lines']:
  594. for subp in path['lines']:
  595. geo = copy(subp)
  596. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  597. path_geo.append(geo)
  598. # the path was painted therefore initialize it
  599. path['lines'] = []
  600. else:
  601. geo = copy(subpath['lines'])
  602. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  603. path_geo.append(geo)
  604. subpath['lines'] = []
  605. if current_subpath == 'bezier':
  606. if path['bezier']:
  607. for subp in path['bezier']:
  608. geo = []
  609. for b in subp:
  610. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  611. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  612. path_geo.append(geo)
  613. # the path was painted therefore initialize it
  614. path['bezier'] = []
  615. else:
  616. geo = []
  617. for b in subpath['bezier']:
  618. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  619. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  620. path_geo.append(geo)
  621. subpath['bezier'] = []
  622. if current_subpath == 'rectangle':
  623. if path['rectangle']:
  624. for subp in path['rectangle']:
  625. geo = copy(subp)
  626. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  627. path_geo.append(geo)
  628. # the path was painted therefore initialize it
  629. path['rectangle'] = []
  630. else:
  631. geo = copy(subpath['rectangle'])
  632. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  633. path_geo.append(geo)
  634. subpath['rectangle'] = []
  635. # store the found geometry
  636. found_aperture = None
  637. if apertures_dict:
  638. for apid in apertures_dict:
  639. # if we already have an aperture with the current size (rounded to 5 decimals)
  640. if apertures_dict[apid]['size'] == round(applied_size, 5):
  641. found_aperture = apid
  642. break
  643. if found_aperture:
  644. apertures_dict[copy(found_aperture)]['solid_geometry'] += path_geo
  645. found_aperture = None
  646. else:
  647. if str(aperture) in apertures_dict.keys():
  648. aperture += 1
  649. apertures_dict[str(aperture)] = {}
  650. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  651. apertures_dict[str(aperture)]['type'] = 'C'
  652. apertures_dict[str(aperture)]['solid_geometry'] = []
  653. apertures_dict[str(aperture)]['solid_geometry'] += path_geo
  654. else:
  655. apertures_dict[str(aperture)] = {}
  656. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  657. apertures_dict[str(aperture)]['type'] = 'C'
  658. apertures_dict[str(aperture)]['solid_geometry'] = []
  659. apertures_dict[str(aperture)]['solid_geometry'] += path_geo
  660. continue
  661. # Fill the path
  662. match = self.fill_path_re.search(pline)
  663. if match:
  664. # scale the size here; some PDF printers apply transformation after the size is declared
  665. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  666. path_geo = list()
  667. if current_subpath == 'lines':
  668. if path['lines']:
  669. for subp in path['lines']:
  670. geo = copy(subp)
  671. # close the subpath if it was not closed already
  672. if close_subpath is False:
  673. geo.append(geo[0])
  674. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  675. path_geo.append(geo_el)
  676. # the path was painted therefore initialize it
  677. path['lines'] = []
  678. else:
  679. geo = copy(subpath['lines'])
  680. # close the subpath if it was not closed already
  681. if close_subpath is False:
  682. geo.append(start_point)
  683. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  684. path_geo.append(geo_el)
  685. subpath['lines'] = []
  686. if current_subpath == 'bezier':
  687. geo = []
  688. if path['bezier']:
  689. for subp in path['bezier']:
  690. for b in subp:
  691. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  692. # close the subpath if it was not closed already
  693. if close_subpath is False:
  694. geo.append(geo[0])
  695. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  696. path_geo.append(geo_el)
  697. # the path was painted therefore initialize it
  698. path['bezier'] = []
  699. else:
  700. for b in subpath['bezier']:
  701. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  702. if close_subpath is False:
  703. geo.append(start_point)
  704. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  705. path_geo.append(geo_el)
  706. subpath['bezier'] = []
  707. if current_subpath == 'rectangle':
  708. if path['rectangle']:
  709. for subp in path['rectangle']:
  710. geo = copy(subp)
  711. # # close the subpath if it was not closed already
  712. # if close_subpath is False and start_point is not None:
  713. # geo.append(start_point)
  714. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  715. path_geo.append(geo_el)
  716. # the path was painted therefore initialize it
  717. path['rectangle'] = []
  718. else:
  719. geo = copy(subpath['rectangle'])
  720. # # close the subpath if it was not closed already
  721. # if close_subpath is False and start_point is not None:
  722. # geo.append(start_point)
  723. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  724. path_geo.append(geo_el)
  725. subpath['rectangle'] = []
  726. # we finished painting and also closed the path if it was the case
  727. close_subpath = True
  728. # if there was a fill color change we look for circular geometries from which we can make drill holes
  729. # for the Excellon file
  730. if flag_clear_geo is True:
  731. # we llok for circular geometries
  732. if current_subpath == 'bezier':
  733. # if there are geometries in the list
  734. if path_geo:
  735. clear_apertures_dict['0']['solid_geometry'] += path_geo
  736. else:
  737. # else, add the geometry as usual
  738. try:
  739. apertures_dict['0']['solid_geometry'] += path_geo
  740. except KeyError:
  741. # in case there is no stroke width yet therefore no aperture
  742. apertures_dict['0'] = {}
  743. apertures_dict['0']['size'] = applied_size
  744. apertures_dict['0']['type'] = 'C'
  745. apertures_dict['0']['solid_geometry'] = []
  746. apertures_dict['0']['solid_geometry'] += path_geo
  747. continue
  748. # Fill and Stroke the path
  749. match = self.fill_stroke_path_re.search(pline)
  750. if match:
  751. # scale the size here; some PDF printers apply transformation after the size is declared
  752. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  753. path_geo = list()
  754. fill_geo = list()
  755. if current_subpath == 'lines':
  756. if path['lines']:
  757. # fill
  758. for subp in path['lines']:
  759. geo = copy(subp)
  760. # close the subpath if it was not closed already
  761. if close_subpath is False:
  762. geo.append(geo[0])
  763. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  764. fill_geo.append(geo_el)
  765. # stroke
  766. for subp in path['lines']:
  767. geo = copy(subp)
  768. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  769. path_geo.append(geo)
  770. # the path was painted therefore initialize it
  771. path['lines'] = []
  772. else:
  773. # fill
  774. geo = copy(subpath['lines'])
  775. # close the subpath if it was not closed already
  776. if close_subpath is False:
  777. geo.append(start_point)
  778. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  779. fill_geo.append(geo_el)
  780. # stroke
  781. geo = copy(subpath['lines'])
  782. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  783. path_geo.append(geo)
  784. subpath['lines'] = []
  785. subpath['lines'] = []
  786. if current_subpath == 'bezier':
  787. geo = []
  788. if path['bezier']:
  789. # fill
  790. for subp in path['bezier']:
  791. for b in subp:
  792. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  793. # close the subpath if it was not closed already
  794. if close_subpath is False:
  795. geo.append(geo[0])
  796. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  797. fill_geo.append(geo_el)
  798. # stroke
  799. for subp in path['bezier']:
  800. geo = []
  801. for b in subp:
  802. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  803. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  804. path_geo.append(geo)
  805. # the path was painted therefore initialize it
  806. path['bezier'] = []
  807. else:
  808. # fill
  809. for b in subpath['bezier']:
  810. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  811. if close_subpath is False:
  812. geo.append(start_point)
  813. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  814. fill_geo.append(geo_el)
  815. # stroke
  816. geo = []
  817. for b in subpath['bezier']:
  818. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  819. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  820. path_geo.append(geo)
  821. subpath['bezier'] = []
  822. if current_subpath == 'rectangle':
  823. if path['rectangle']:
  824. # fill
  825. for subp in path['rectangle']:
  826. geo = copy(subp)
  827. # # close the subpath if it was not closed already
  828. # if close_subpath is False:
  829. # geo.append(geo[0])
  830. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  831. fill_geo.append(geo_el)
  832. # stroke
  833. for subp in path['rectangle']:
  834. geo = copy(subp)
  835. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  836. path_geo.append(geo)
  837. # the path was painted therefore initialize it
  838. path['rectangle'] = []
  839. else:
  840. # fill
  841. geo = copy(subpath['rectangle'])
  842. # # close the subpath if it was not closed already
  843. # if close_subpath is False:
  844. # geo.append(start_point)
  845. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  846. fill_geo.append(geo_el)
  847. # stroke
  848. geo = copy(subpath['rectangle'])
  849. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  850. path_geo.append(geo)
  851. subpath['rectangle'] = []
  852. # we finished painting and also closed the path if it was the case
  853. close_subpath = True
  854. # store the found geometry for stroking the path
  855. found_aperture = None
  856. if apertures_dict:
  857. for apid in apertures_dict:
  858. # if we already have an aperture with the current size (rounded to 5 decimals)
  859. if apertures_dict[apid]['size'] == round(applied_size, 5):
  860. found_aperture = apid
  861. break
  862. if found_aperture:
  863. apertures_dict[copy(found_aperture)]['solid_geometry'] += path_geo
  864. found_aperture = None
  865. else:
  866. if str(aperture) in apertures_dict.keys():
  867. aperture += 1
  868. apertures_dict[str(aperture)] = {}
  869. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  870. apertures_dict[str(aperture)]['type'] = 'C'
  871. apertures_dict[str(aperture)]['solid_geometry'] = []
  872. apertures_dict[str(aperture)]['solid_geometry'] += path_geo
  873. else:
  874. apertures_dict[str(aperture)] = {}
  875. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  876. apertures_dict[str(aperture)]['type'] = 'C'
  877. apertures_dict[str(aperture)]['solid_geometry'] = []
  878. apertures_dict[str(aperture)]['solid_geometry'] += path_geo
  879. # store the found geometry for filling the path
  880. try:
  881. apertures_dict['0']['solid_geometry'] += fill_geo
  882. except KeyError:
  883. # in case there is no stroke width yet therefore no aperture
  884. apertures_dict['0'] = {}
  885. apertures_dict['0']['size'] = round(applied_size, 5)
  886. apertures_dict['0']['type'] = 'C'
  887. apertures_dict['0']['solid_geometry'] = []
  888. apertures_dict['0']['solid_geometry'] += fill_geo
  889. continue
  890. # tidy up. copy the current aperture dict to the object dict but only if it is not empty
  891. if apertures_dict:
  892. object_dict[layer_nr] = deepcopy(apertures_dict)
  893. if clear_apertures_dict['0']['solid_geometry']:
  894. object_dict[0] = deepcopy(clear_apertures_dict)
  895. # delete keys (layers) with empty values
  896. empty_layers = []
  897. for layer in object_dict:
  898. if not object_dict[layer]:
  899. empty_layers.append(layer)
  900. for x in empty_layers:
  901. if x in object_dict:
  902. object_dict.pop(x)
  903. return object_dict
  904. def bezier_to_points(self, start, c1, c2, stop):
  905. """
  906. # Equation Bezier, page 184 PDF 1.4 reference
  907. # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  908. # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0
  909. # in the following equation:
  910. # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2 + P3*t ** 3
  911. # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides
  912. # with the final point P3. Intermediate values of t generate intermediate points along the curve.
  913. # The curve does not, in general, pass through the two control points P1 and P2
  914. :return: LineString geometry
  915. """
  916. # here we store the geometric points
  917. points = []
  918. nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles))
  919. for t in nr_points:
  920. term_p0 = (1 - t) ** 3
  921. term_p1 = 3 * t * (1 - t) ** 2
  922. term_p2 = 3 * (1 - t) * t ** 2
  923. term_p3 = t ** 3
  924. x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3
  925. y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3
  926. points.append([x, y])
  927. return points
  928. # def bezier_to_circle(self, path):
  929. # lst = []
  930. # for el in range(len(path)):
  931. # if type(path) is list:
  932. # for coord in path[el]:
  933. # lst.append(coord)
  934. # else:
  935. # lst.append(el)
  936. #
  937. # if lst:
  938. # minx = min(lst, key=lambda t: t[0])[0]
  939. # miny = min(lst, key=lambda t: t[1])[1]
  940. # maxx = max(lst, key=lambda t: t[0])[0]
  941. # maxy = max(lst, key=lambda t: t[1])[1]
  942. # center = (maxx-minx, maxy-miny)
  943. # radius = (maxx-minx) / 2
  944. # return [center, radius]
  945. #
  946. # def circle_to_points(self, center, radius):
  947. # geo = Point(center).buffer(radius, resolution=self.step_per_circles)
  948. # return LineString(list(geo.exterior.coords))
  949. #