ToolPDF.py 49 KB

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