ToolPDF.py 48 KB

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