ToolPDF.py 66 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 4/23/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore
  8. from FlatCAMTool import FlatCAMTool
  9. from FlatCAMCommon import GracefulException as grace
  10. from shapely.geometry import Point, Polygon, LineString, MultiPolygon
  11. from shapely.ops import unary_union
  12. from copy import copy, deepcopy
  13. import numpy as np
  14. import zlib
  15. import re
  16. import time
  17. import logging
  18. import traceback
  19. import gettext
  20. import FlatCAMTranslation as fcTranslate
  21. import builtins
  22. fcTranslate.apply_language('strings')
  23. if '_' not in builtins.__dict__:
  24. _ = gettext.gettext
  25. log = logging.getLogger('base')
  26. class ToolPDF(FlatCAMTool):
  27. """
  28. Parse a PDF file.
  29. Reference here: https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  30. Return a list of geometries
  31. """
  32. toolName = _("PDF Import Tool")
  33. def __init__(self, app):
  34. FlatCAMTool.__init__(self, app)
  35. self.app = app
  36. self.decimals = self.app.decimals
  37. self.step_per_circles = self.app.defaults["gerber_circle_steps"]
  38. self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
  39. # detect stroke color change; it means a new object to be created
  40. self.stroke_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*RG$')
  41. # detect fill color change; we check here for white color (transparent geometry);
  42. # if detected we create an Excellon from it
  43. self.fill_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*rg$')
  44. # detect 're' command
  45. self.rect_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*re$')
  46. # detect 'm' command
  47. self.start_subpath_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sm$')
  48. # detect 'l' command
  49. self.draw_line_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sl')
  50. # detect 'c' command
  51. self.draw_arc_3pt_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)'
  52. r'\s(-?\d+\.?\d*)\s*c$')
  53. # detect 'v' command
  54. self.draw_arc_2pt_c1start_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*v$')
  55. # detect 'y' command
  56. self.draw_arc_2pt_c2stop_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*y$')
  57. # detect 'h' command
  58. self.end_subpath_re = re.compile(r'^h$')
  59. # detect 'w' command
  60. self.strokewidth_re = re.compile(r'^(\d+\.?\d*)\s*w$')
  61. # detect 'S' command
  62. self.stroke_path__re = re.compile(r'^S\s?[Q]?$')
  63. # detect 's' command
  64. self.close_stroke_path__re = re.compile(r'^s$')
  65. # detect 'f' or 'f*' command
  66. self.fill_path_re = re.compile(r'^[f|F][*]?$')
  67. # detect 'B' or 'B*' command
  68. self.fill_stroke_path_re = re.compile(r'^B[*]?$')
  69. # detect 'b' or 'b*' command
  70. self.close_fill_stroke_path_re = re.compile(r'^b[*]?$')
  71. # detect 'n'
  72. self.no_op_re = re.compile(r'^n$')
  73. # detect offset transformation. Pattern: (1) (0) (0) (1) (x) (y)
  74. # self.offset_re = re.compile(r'^1\.?0*\s0?\.?0*\s0?\.?0*\s1\.?0*\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*cm$')
  75. # detect scale transformation. Pattern: (factor_x) (0) (0) (factor_y) (0) (0)
  76. # self.scale_re = re.compile(r'^q? (-?\d+\.?\d*) 0\.?0* 0\.?0* (-?\d+\.?\d*) 0\.?0* 0\.?0*\s+cm$')
  77. # detect combined transformation. Should always be the last
  78. self.combined_transform_re = re.compile(r'^(q)?\s*(-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) '
  79. r'(-?\d+\.?\d*) (-?\d+\.?\d*)\s+cm$')
  80. # detect clipping path
  81. self.clip_path_re = re.compile(r'^W[*]? n?$')
  82. # detect save graphic state in graphic stack
  83. self.save_gs_re = re.compile(r'^q.*?$')
  84. # detect restore graphic state from graphic stack
  85. self.restore_gs_re = re.compile(r'^.*Q.*$')
  86. # graphic stack where we save parameters like transformation, line_width
  87. self.gs = {}
  88. # each element is a list composed of sublist elements
  89. # (each sublist has 2 lists each having 2 elements: first is offset like:
  90. # offset_geo = [off_x, off_y], second element is scale list with 2 elements, like: scale_geo = [sc_x, sc_yy])
  91. self.gs['transform'] = []
  92. self.gs['line_width'] = [] # each element is a float
  93. self.pdf_decompressed = {}
  94. # key = file name and extension
  95. # value is a dict to store the parsed content of the PDF
  96. self.pdf_parsed = {}
  97. # QTimer for periodic check
  98. self.check_thread = QtCore.QTimer()
  99. # Every time a parser is started we add a promise; every time a parser finished we remove a promise
  100. # when empty we start the layer rendering
  101. self.parsing_promises = []
  102. # conversion factor to INCH
  103. self.point_to_unit_factor = 0.01388888888
  104. def run(self, toggle=True):
  105. self.app.report_usage("ToolPDF()")
  106. self.set_tool_ui()
  107. self.on_open_pdf_click()
  108. def install(self, icon=None, separator=None, **kwargs):
  109. FlatCAMTool.install(self, icon, separator, shortcut='Ctrl+Q', **kwargs)
  110. def set_tool_ui(self):
  111. pass
  112. def on_open_pdf_click(self):
  113. """
  114. File menu callback for opening an PDF file.
  115. :return: None
  116. """
  117. self.app.report_usage("ToolPDF.on_open_pdf_click()")
  118. self.app.log.debug("ToolPDF.on_open_pdf_click()")
  119. _filter_ = "Adobe PDF Files (*.pdf);;" \
  120. "All Files (*.*)"
  121. try:
  122. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
  123. directory=self.app.get_last_folder(),
  124. filter=_filter_)
  125. except TypeError:
  126. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
  127. if len(filenames) == 0:
  128. self.app.inform.emit('[WARNING_NOTCL] %s.' % _("Open PDF cancelled"))
  129. else:
  130. # start the parsing timer with a period of 1 second
  131. self.periodic_check(1000)
  132. for filename in filenames:
  133. if filename != '':
  134. self.app.worker_task.emit({'fcn': self.open_pdf,
  135. 'params': [filename]})
  136. def open_pdf(self, filename):
  137. short_name = filename.split('/')[-1].split('\\')[-1]
  138. self.parsing_promises.append(short_name)
  139. self.pdf_parsed[short_name] = {}
  140. self.pdf_parsed[short_name]['pdf'] = {}
  141. self.pdf_parsed[short_name]['filename'] = filename
  142. self.pdf_decompressed[short_name] = ''
  143. # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
  144. if self.app.defaults['units'].upper() == 'MM':
  145. # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm
  146. self.point_to_unit_factor = 25.4 / 72
  147. else:
  148. # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch
  149. self.point_to_unit_factor = 1 / 72
  150. if self.app.abort_flag:
  151. # graceful abort requested by the user
  152. raise grace
  153. with self.app.proc_container.new(_("Parsing PDF file ...")):
  154. with open(filename, "rb") as f:
  155. pdf = f.read()
  156. stream_nr = 0
  157. for s in re.findall(self.stream_re, pdf):
  158. if self.app.abort_flag:
  159. # graceful abort requested by the user
  160. raise grace
  161. stream_nr += 1
  162. log.debug(" PDF STREAM: %d\n" % stream_nr)
  163. s = s.strip(b'\r\n')
  164. try:
  165. self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n')
  166. except Exception as e:
  167. log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
  168. self.pdf_parsed[short_name]['pdf'] = self.parse_pdf(pdf_content=self.pdf_decompressed[short_name])
  169. # we used it, now we delete it
  170. self.pdf_decompressed[short_name] = ''
  171. # removal from list is done in a multithreaded way therefore not always the removal can be done
  172. # try to remove until it's done
  173. try:
  174. while True:
  175. self.parsing_promises.remove(short_name)
  176. time.sleep(0.1)
  177. except Exception as e:
  178. log.debug("ToolPDF.open_pdf() --> %s" % str(e))
  179. self.app.inform.emit('[success] %s: %s' % (_("Opened"), str(filename)))
  180. def layer_rendering_as_excellon(self, filename, ap_dict, layer_nr):
  181. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  182. # store the points here until reconstitution:
  183. # keys are diameters and values are list of (x,y) coords
  184. points = {}
  185. def obj_init(exc_obj, app_obj):
  186. clear_geo = [geo_el['clear'] for geo_el in ap_dict['0']['geometry']]
  187. for geo in clear_geo:
  188. xmin, ymin, xmax, ymax = geo.bounds
  189. center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
  190. # for drill bits, even in INCH, it's enough 3 decimals
  191. correction_factor = 0.974
  192. dia = (xmax - xmin) * correction_factor
  193. dia = round(dia, 3)
  194. if dia in points:
  195. points[dia].append(center)
  196. else:
  197. points[dia] = [center]
  198. sorted_dia = sorted(points.keys())
  199. name_tool = 0
  200. for dia in sorted_dia:
  201. name_tool += 1
  202. # create tools dictionary
  203. spec = {"C": dia, 'solid_geometry': []}
  204. exc_obj.tools[str(name_tool)] = spec
  205. # create drill list of dictionaries
  206. for dia_points in points:
  207. if dia == dia_points:
  208. for pt in points[dia_points]:
  209. exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
  210. break
  211. ret = exc_obj.create_geometry()
  212. if ret == 'fail':
  213. log.debug("Could not create geometry for Excellon object.")
  214. return "fail"
  215. for tool in exc_obj.tools:
  216. if exc_obj.tools[tool]['solid_geometry']:
  217. return
  218. app_obj.inform.emit('[ERROR_NOTCL] %s: %s' %
  219. (_("No geometry found in file"), outname))
  220. return "fail"
  221. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  222. ret_val = self.app.new_object("excellon", outname, obj_init, autoselected=False)
  223. if ret_val == 'fail':
  224. self.app.inform.emit('[ERROR_NOTCL] %s' %
  225. _('Open PDF file failed.'))
  226. return
  227. # Register recent file
  228. self.app.file_opened.emit("excellon", filename)
  229. # GUI feedback
  230. self.app.inform.emit('[success] %s: %s' %
  231. (_("Rendered"), outname))
  232. def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
  233. outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
  234. def obj_init(grb_obj):
  235. grb_obj.apertures = ap_dict
  236. poly_buff = []
  237. follow_buf = []
  238. for ap in grb_obj.apertures:
  239. for k in grb_obj.apertures[ap]:
  240. if k == 'geometry':
  241. for geo_el in ap_dict[ap][k]:
  242. if 'solid' in geo_el:
  243. poly_buff.append(geo_el['solid'])
  244. if 'follow' in geo_el:
  245. follow_buf.append(geo_el['follow'])
  246. poly_buff = unary_union(poly_buff)
  247. if '0' in grb_obj.apertures:
  248. global_clear_geo = []
  249. if 'geometry' in grb_obj.apertures['0']:
  250. for geo_el in ap_dict['0']['geometry']:
  251. if 'clear' in geo_el:
  252. global_clear_geo.append(geo_el['clear'])
  253. if global_clear_geo:
  254. solid = []
  255. for apid in grb_obj.apertures:
  256. if 'geometry' in grb_obj.apertures[apid]:
  257. for elem in grb_obj.apertures[apid]['geometry']:
  258. if 'solid' in elem:
  259. solid_geo = deepcopy(elem['solid'])
  260. for clear_geo in global_clear_geo:
  261. # Make sure that the clear_geo is within the solid_geo otherwise we loose
  262. # the solid_geometry. We want for clear_geometry just to cut into solid_geometry
  263. # not to delete it
  264. if clear_geo.within(solid_geo):
  265. solid_geo = solid_geo.difference(clear_geo)
  266. if solid_geo.is_empty:
  267. solid_geo = elem['solid']
  268. try:
  269. for poly in solid_geo:
  270. solid.append(poly)
  271. except TypeError:
  272. solid.append(solid_geo)
  273. poly_buff = deepcopy(MultiPolygon(solid))
  274. follow_buf = unary_union(follow_buf)
  275. try:
  276. poly_buff = poly_buff.buffer(0.0000001)
  277. except ValueError:
  278. pass
  279. try:
  280. poly_buff = poly_buff.buffer(-0.0000001)
  281. except ValueError:
  282. pass
  283. grb_obj.solid_geometry = deepcopy(poly_buff)
  284. grb_obj.follow_geometry = deepcopy(follow_buf)
  285. with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
  286. ret = self.app.new_object('gerber', outname, obj_init, autoselected=False)
  287. if ret == 'fail':
  288. self.app.inform.emit('[ERROR_NOTCL] %s' %
  289. _('Open PDF file failed.'))
  290. return
  291. # Register recent file
  292. self.app.file_opened.emit('gerber', filename)
  293. # GUI feedback
  294. self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
  295. def periodic_check(self, check_period):
  296. """
  297. This function starts an QTimer and it will periodically check if parsing was done
  298. :param check_period: time at which to check periodically if all plots finished to be plotted
  299. :return:
  300. """
  301. # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
  302. # self.plot_thread.start()
  303. log.debug("ToolPDF --> Periodic Check started.")
  304. try:
  305. self.check_thread.stop()
  306. except TypeError:
  307. pass
  308. self.check_thread.setInterval(check_period)
  309. try:
  310. self.check_thread.timeout.disconnect(self.periodic_check_handler)
  311. except (TypeError, AttributeError):
  312. pass
  313. self.check_thread.timeout.connect(self.periodic_check_handler)
  314. self.check_thread.start(QtCore.QThread.HighPriority)
  315. def periodic_check_handler(self):
  316. """
  317. If the parsing worker finished then start multithreaded rendering
  318. :return:
  319. """
  320. # log.debug("checking parsing --> %s" % str(self.parsing_promises))
  321. try:
  322. if not self.parsing_promises:
  323. self.check_thread.stop()
  324. # parsing finished start the layer rendering
  325. if self.pdf_parsed:
  326. obj_to_delete = []
  327. for object_name in self.pdf_parsed:
  328. if self.app.abort_flag:
  329. # graceful abort requested by the user
  330. raise grace
  331. filename = deepcopy(self.pdf_parsed[object_name]['filename'])
  332. pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
  333. obj_to_delete.append(object_name)
  334. for k in pdf_content:
  335. if self.app.abort_flag:
  336. # graceful abort requested by the user
  337. raise grace
  338. ap_dict = pdf_content[k]
  339. if ap_dict:
  340. layer_nr = k
  341. if k == 0:
  342. self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
  343. 'params': [filename, ap_dict, layer_nr]})
  344. else:
  345. self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
  346. 'params': [filename, ap_dict, layer_nr]})
  347. # delete the object already processed so it will not be processed again for other objects
  348. # that were opened at the same time; like in drag & drop on GUI
  349. for obj_name in obj_to_delete:
  350. if obj_name in self.pdf_parsed:
  351. self.pdf_parsed.pop(obj_name)
  352. log.debug("ToolPDF --> Periodic check finished.")
  353. except Exception:
  354. traceback.print_exc()
  355. def parse_pdf(self, pdf_content):
  356. path = {}
  357. path['lines'] = [] # it's a list of lines subpaths
  358. path['bezier'] = [] # it's a list of bezier arcs subpaths
  359. path['rectangle'] = [] # it's a list of rectangle subpaths
  360. subpath = {}
  361. subpath['lines'] = [] # it's a list of points
  362. subpath['bezier'] = [] # it's a list of sublists each like this [start, c1, c2, stop]
  363. subpath['rectangle'] = [] # it's a list of sublists of points
  364. # store the start point (when 'm' command is encountered)
  365. current_subpath = None
  366. # set True when 'h' command is encountered (close subpath)
  367. close_subpath = False
  368. start_point = None
  369. current_point = None
  370. size = 0
  371. # initial values for the transformations, in case they are not encountered in the PDF file
  372. offset_geo = [0, 0]
  373. scale_geo = [1, 1]
  374. # store the objects to be transformed into Gerbers
  375. object_dict = {}
  376. # will serve as key in the object_dict
  377. layer_nr = 1
  378. # create first object
  379. object_dict[layer_nr] = {}
  380. # store the apertures here
  381. apertures_dict = {}
  382. # initial aperture
  383. aperture = 10
  384. # store the apertures with clear geometry here
  385. # we are interested only in the circular geometry (drill holes) therefore we target only Bezier subpaths
  386. clear_apertures_dict = {}
  387. # everything will be stored in the '0' aperture since we are dealing with clear polygons not strokes
  388. clear_apertures_dict['0'] = {}
  389. clear_apertures_dict['0']['size'] = 0.0
  390. clear_apertures_dict['0']['type'] = 'C'
  391. clear_apertures_dict['0']['geometry'] = []
  392. # on stroke color change we create a new apertures dictionary and store the old one in a storage from where
  393. # it will be transformed into Gerber object
  394. old_color = [None, None, None]
  395. # signal that we have clear geometry and the geometry will be added to a special layer_nr = 0
  396. flag_clear_geo = False
  397. line_nr = 0
  398. lines = pdf_content.splitlines()
  399. for pline in lines:
  400. if self.app.abort_flag:
  401. # graceful abort requested by the user
  402. raise grace
  403. line_nr += 1
  404. log.debug("line %d: %s" % (line_nr, pline))
  405. # COLOR DETECTION / OBJECT DETECTION
  406. match = self.stroke_color_re.search(pline)
  407. if match:
  408. color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
  409. log.debug(
  410. "ToolPDF.parse_pdf() --> STROKE Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
  411. (line_nr, color[0], color[1], color[2]))
  412. if color[0] == old_color[0] and color[1] == old_color[1] and color[2] == old_color[2]:
  413. # same color, do nothing
  414. continue
  415. else:
  416. if apertures_dict:
  417. object_dict[layer_nr] = deepcopy(apertures_dict)
  418. apertures_dict.clear()
  419. layer_nr += 1
  420. object_dict[layer_nr] = {}
  421. old_color = copy(color)
  422. # we make sure that the following geometry is added to the right storage
  423. flag_clear_geo = False
  424. continue
  425. # CLEAR GEOMETRY detection
  426. match = self.fill_color_re.search(pline)
  427. if match:
  428. fill_color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
  429. log.debug(
  430. "ToolPDF.parse_pdf() --> FILL Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
  431. (line_nr, fill_color[0], fill_color[1], fill_color[2]))
  432. # if the color is white we are seeing 'clear_geometry' that can't be seen. It may be that those
  433. # geometries are actually holes from which we can make an Excellon file
  434. if fill_color[0] == 1 and fill_color[1] == 1 and fill_color[2] == 1:
  435. flag_clear_geo = True
  436. else:
  437. flag_clear_geo = False
  438. continue
  439. # TRANSFORMATIONS DETECTION #
  440. # Detect combined transformation.
  441. match = self.combined_transform_re.search(pline)
  442. if match:
  443. # detect save graphic stack event
  444. # sometimes they combine save_to_graphics_stack with the transformation on the same line
  445. if match.group(1) == 'q':
  446. log.debug(
  447. "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
  448. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  449. self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
  450. self.gs['line_width'].append(deepcopy(size))
  451. # transformation = TRANSLATION (OFFSET)
  452. if (float(match.group(3)) == 0 and float(match.group(4)) == 0) and \
  453. (float(match.group(6)) != 0 or float(match.group(7)) != 0):
  454. log.debug(
  455. "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
  456. offset_geo[0] += float(match.group(6))
  457. offset_geo[1] += float(match.group(7))
  458. # log.debug("Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
  459. # transformation = SCALING
  460. if float(match.group(2)) != 1 and float(match.group(5)) != 1:
  461. log.debug(
  462. "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
  463. scale_geo[0] *= float(match.group(2))
  464. scale_geo[1] *= float(match.group(5))
  465. # log.debug("Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
  466. continue
  467. # detect save graphic stack event
  468. match = self.save_gs_re.search(pline)
  469. if match:
  470. log.debug(
  471. "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
  472. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  473. self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
  474. self.gs['line_width'].append(deepcopy(size))
  475. # detect restore from graphic stack event
  476. match = self.restore_gs_re.search(pline)
  477. if match:
  478. try:
  479. restored_transform = self.gs['transform'].pop(-1)
  480. offset_geo = restored_transform[0]
  481. scale_geo = restored_transform[1]
  482. except IndexError:
  483. # nothing to remove
  484. log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
  485. pass
  486. try:
  487. size = self.gs['line_width'].pop(-1)
  488. except IndexError:
  489. log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
  490. # nothing to remove
  491. pass
  492. log.debug(
  493. "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> "
  494. "restored_offset=[%f, %f] ||| restored_scale=[%f, %f]" %
  495. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  496. # log.debug("Restored Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
  497. # log.debug("Restored Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
  498. # PATH CONSTRUCTION #
  499. # Start SUBPATH
  500. match = self.start_subpath_re.search(pline)
  501. if match:
  502. # we just started a subpath so we mark it as not closed yet
  503. close_subpath = False
  504. # init subpaths
  505. subpath['lines'] = []
  506. subpath['bezier'] = []
  507. subpath['rectangle'] = []
  508. # detect start point to move to
  509. x = float(match.group(1)) + offset_geo[0]
  510. y = float(match.group(2)) + offset_geo[1]
  511. pt = (x * self.point_to_unit_factor * scale_geo[0],
  512. y * self.point_to_unit_factor * scale_geo[1])
  513. start_point = pt
  514. # add the start point to subpaths
  515. subpath['lines'].append(start_point)
  516. # subpath['bezier'].append(start_point)
  517. # subpath['rectangle'].append(start_point)
  518. current_point = start_point
  519. continue
  520. # Draw Line
  521. match = self.draw_line_re.search(pline)
  522. if match:
  523. current_subpath = 'lines'
  524. x = float(match.group(1)) + offset_geo[0]
  525. y = float(match.group(2)) + offset_geo[1]
  526. pt = (x * self.point_to_unit_factor * scale_geo[0],
  527. y * self.point_to_unit_factor * scale_geo[1])
  528. subpath['lines'].append(pt)
  529. current_point = pt
  530. continue
  531. # Draw Bezier 'c'
  532. match = self.draw_arc_3pt_re.search(pline)
  533. if match:
  534. current_subpath = 'bezier'
  535. start = current_point
  536. x = float(match.group(1)) + offset_geo[0]
  537. y = float(match.group(2)) + offset_geo[1]
  538. c1 = (x * self.point_to_unit_factor * scale_geo[0],
  539. y * self.point_to_unit_factor * scale_geo[1])
  540. x = float(match.group(3)) + offset_geo[0]
  541. y = float(match.group(4)) + offset_geo[1]
  542. c2 = (x * self.point_to_unit_factor * scale_geo[0],
  543. y * self.point_to_unit_factor * scale_geo[1])
  544. x = float(match.group(5)) + offset_geo[0]
  545. y = float(match.group(6)) + offset_geo[1]
  546. stop = (x * self.point_to_unit_factor * scale_geo[0],
  547. y * self.point_to_unit_factor * scale_geo[1])
  548. subpath['bezier'].append([start, c1, c2, stop])
  549. current_point = stop
  550. continue
  551. # Draw Bezier 'v'
  552. match = self.draw_arc_2pt_c1start_re.search(pline)
  553. if match:
  554. current_subpath = 'bezier'
  555. start = current_point
  556. x = float(match.group(1)) + offset_geo[0]
  557. y = float(match.group(2)) + offset_geo[1]
  558. c2 = (x * self.point_to_unit_factor * scale_geo[0],
  559. y * self.point_to_unit_factor * scale_geo[1])
  560. x = float(match.group(3)) + offset_geo[0]
  561. y = float(match.group(4)) + offset_geo[1]
  562. stop = (x * self.point_to_unit_factor * scale_geo[0],
  563. y * self.point_to_unit_factor * scale_geo[1])
  564. subpath['bezier'].append([start, start, c2, stop])
  565. current_point = stop
  566. continue
  567. # Draw Bezier 'y'
  568. match = self.draw_arc_2pt_c2stop_re.search(pline)
  569. if match:
  570. start = current_point
  571. x = float(match.group(1)) + offset_geo[0]
  572. y = float(match.group(2)) + offset_geo[1]
  573. c1 = (x * self.point_to_unit_factor * scale_geo[0],
  574. y * self.point_to_unit_factor * scale_geo[1])
  575. x = float(match.group(3)) + offset_geo[0]
  576. y = float(match.group(4)) + offset_geo[1]
  577. stop = (x * self.point_to_unit_factor * scale_geo[0],
  578. y * self.point_to_unit_factor * scale_geo[1])
  579. subpath['bezier'].append([start, c1, stop, stop])
  580. current_point = stop
  581. continue
  582. # Draw Rectangle 're'
  583. match = self.rect_re.search(pline)
  584. if match:
  585. current_subpath = 'rectangle'
  586. x = (float(match.group(1)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0]
  587. y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1]
  588. width = (float(match.group(3)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0]
  589. height = (float(match.group(4)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1]
  590. pt1 = (x, y)
  591. pt2 = (x+width, y)
  592. pt3 = (x+width, y+height)
  593. pt4 = (x, y+height)
  594. subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1]
  595. current_point = pt1
  596. continue
  597. # Detect clipping path set
  598. # ignore this and delete the current subpath
  599. match = self.clip_path_re.search(pline)
  600. if match:
  601. subpath['lines'] = []
  602. subpath['bezier'] = []
  603. subpath['rectangle'] = []
  604. # it means that we've already added the subpath to path and we need to delete it
  605. # clipping path is usually either rectangle or lines
  606. if close_subpath is True:
  607. close_subpath = False
  608. if current_subpath == 'lines':
  609. path['lines'].pop(-1)
  610. if current_subpath == 'rectangle':
  611. path['rectangle'].pop(-1)
  612. continue
  613. # Close SUBPATH
  614. match = self.end_subpath_re.search(pline)
  615. if match:
  616. close_subpath = True
  617. if current_subpath == 'lines':
  618. subpath['lines'].append(start_point)
  619. # since we are closing the subpath add it to the path, a path may have chained subpaths
  620. path['lines'].append(copy(subpath['lines']))
  621. subpath['lines'] = []
  622. elif current_subpath == 'bezier':
  623. # subpath['bezier'].append(start_point)
  624. # since we are closing the subpath add it to the path, a path may have chained subpaths
  625. path['bezier'].append(copy(subpath['bezier']))
  626. subpath['bezier'] = []
  627. elif current_subpath == 'rectangle':
  628. # subpath['rectangle'].append(start_point)
  629. # since we are closing the subpath add it to the path, a path may have chained subpaths
  630. path['rectangle'].append(copy(subpath['rectangle']))
  631. subpath['rectangle'] = []
  632. continue
  633. # PATH PAINTING #
  634. # Detect Stroke width / aperture
  635. match = self.strokewidth_re.search(pline)
  636. if match:
  637. size = float(match.group(1))
  638. continue
  639. # Detect No_Op command, ignore the current subpath
  640. match = self.no_op_re.search(pline)
  641. if match:
  642. subpath['lines'] = []
  643. subpath['bezier'] = []
  644. subpath['rectangle'] = []
  645. continue
  646. # Stroke the path
  647. match = self.stroke_path__re.search(pline)
  648. if match:
  649. # scale the size here; some PDF printers apply transformation after the size is declared
  650. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  651. path_geo = []
  652. if current_subpath == 'lines':
  653. if path['lines']:
  654. for subp in path['lines']:
  655. geo = copy(subp)
  656. try:
  657. geo = LineString(geo).buffer((float(applied_size) / 2),
  658. resolution=self.step_per_circles)
  659. path_geo.append(geo)
  660. except ValueError:
  661. pass
  662. # the path was painted therefore initialize it
  663. path['lines'] = []
  664. else:
  665. geo = copy(subpath['lines'])
  666. try:
  667. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  668. path_geo.append(geo)
  669. except ValueError:
  670. pass
  671. subpath['lines'] = []
  672. if current_subpath == 'bezier':
  673. if path['bezier']:
  674. for subp in path['bezier']:
  675. geo = []
  676. for b in subp:
  677. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  678. try:
  679. geo = LineString(geo).buffer((float(applied_size) / 2),
  680. resolution=self.step_per_circles)
  681. path_geo.append(geo)
  682. except ValueError:
  683. pass
  684. # the path was painted therefore initialize it
  685. path['bezier'] = []
  686. else:
  687. geo = []
  688. for b in subpath['bezier']:
  689. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  690. try:
  691. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  692. path_geo.append(geo)
  693. except ValueError:
  694. pass
  695. subpath['bezier'] = []
  696. if current_subpath == 'rectangle':
  697. if path['rectangle']:
  698. for subp in path['rectangle']:
  699. geo = copy(subp)
  700. try:
  701. geo = LineString(geo).buffer((float(applied_size) / 2),
  702. resolution=self.step_per_circles)
  703. path_geo.append(geo)
  704. except ValueError:
  705. pass
  706. # the path was painted therefore initialize it
  707. path['rectangle'] = []
  708. else:
  709. geo = copy(subpath['rectangle'])
  710. try:
  711. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  712. path_geo.append(geo)
  713. except ValueError:
  714. pass
  715. subpath['rectangle'] = []
  716. # store the found geometry
  717. found_aperture = None
  718. if apertures_dict:
  719. for apid in apertures_dict:
  720. # if we already have an aperture with the current size (rounded to 5 decimals)
  721. if apertures_dict[apid]['size'] == round(applied_size, 5):
  722. found_aperture = apid
  723. break
  724. if found_aperture:
  725. for pdf_geo in path_geo:
  726. if isinstance(pdf_geo, MultiPolygon):
  727. for poly in pdf_geo:
  728. new_el = {}
  729. new_el['solid'] = poly
  730. new_el['follow'] = poly.exterior
  731. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  732. else:
  733. new_el = {}
  734. new_el['solid'] = pdf_geo
  735. new_el['follow'] = pdf_geo.exterior
  736. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  737. else:
  738. if str(aperture) in apertures_dict.keys():
  739. aperture += 1
  740. apertures_dict[str(aperture)] = {}
  741. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  742. apertures_dict[str(aperture)]['type'] = 'C'
  743. apertures_dict[str(aperture)]['geometry'] = []
  744. for pdf_geo in path_geo:
  745. if isinstance(pdf_geo, MultiPolygon):
  746. for poly in pdf_geo:
  747. new_el = {}
  748. new_el['solid'] = poly
  749. new_el['follow'] = poly.exterior
  750. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  751. else:
  752. new_el = {}
  753. new_el['solid'] = pdf_geo
  754. new_el['follow'] = pdf_geo.exterior
  755. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  756. else:
  757. apertures_dict[str(aperture)] = {}
  758. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  759. apertures_dict[str(aperture)]['type'] = 'C'
  760. apertures_dict[str(aperture)]['geometry'] = []
  761. for pdf_geo in path_geo:
  762. if isinstance(pdf_geo, MultiPolygon):
  763. for poly in pdf_geo:
  764. new_el = {}
  765. new_el['solid'] = poly
  766. new_el['follow'] = poly.exterior
  767. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  768. else:
  769. new_el = {}
  770. new_el['solid'] = pdf_geo
  771. new_el['follow'] = pdf_geo.exterior
  772. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  773. continue
  774. # Fill the path
  775. match = self.fill_path_re.search(pline)
  776. if match:
  777. # scale the size here; some PDF printers apply transformation after the size is declared
  778. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  779. path_geo = []
  780. if current_subpath == 'lines':
  781. if path['lines']:
  782. for subp in path['lines']:
  783. geo = copy(subp)
  784. # close the subpath if it was not closed already
  785. if close_subpath is False:
  786. geo.append(geo[0])
  787. try:
  788. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  789. path_geo.append(geo_el)
  790. except ValueError:
  791. pass
  792. # the path was painted therefore initialize it
  793. path['lines'] = []
  794. else:
  795. geo = copy(subpath['lines'])
  796. # close the subpath if it was not closed already
  797. if close_subpath is False:
  798. geo.append(start_point)
  799. try:
  800. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  801. path_geo.append(geo_el)
  802. except ValueError:
  803. pass
  804. subpath['lines'] = []
  805. if current_subpath == 'bezier':
  806. geo = []
  807. if path['bezier']:
  808. for subp in path['bezier']:
  809. for b in subp:
  810. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  811. # close the subpath if it was not closed already
  812. if close_subpath is False:
  813. geo.append(geo[0])
  814. try:
  815. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  816. path_geo.append(geo_el)
  817. except ValueError:
  818. pass
  819. # the path was painted therefore initialize it
  820. path['bezier'] = []
  821. else:
  822. for b in subpath['bezier']:
  823. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  824. if close_subpath is False:
  825. geo.append(start_point)
  826. try:
  827. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  828. path_geo.append(geo_el)
  829. except ValueError:
  830. pass
  831. subpath['bezier'] = []
  832. if current_subpath == 'rectangle':
  833. if path['rectangle']:
  834. for subp in path['rectangle']:
  835. geo = copy(subp)
  836. # # close the subpath if it was not closed already
  837. # if close_subpath is False and start_point is not None:
  838. # geo.append(start_point)
  839. try:
  840. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  841. path_geo.append(geo_el)
  842. except ValueError:
  843. pass
  844. # the path was painted therefore initialize it
  845. path['rectangle'] = []
  846. else:
  847. geo = copy(subpath['rectangle'])
  848. # # close the subpath if it was not closed already
  849. # if close_subpath is False and start_point is not None:
  850. # geo.append(start_point)
  851. try:
  852. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  853. path_geo.append(geo_el)
  854. except ValueError:
  855. pass
  856. subpath['rectangle'] = []
  857. # we finished painting and also closed the path if it was the case
  858. close_subpath = True
  859. # in case that a color change to white (transparent) occurred
  860. if flag_clear_geo is True:
  861. # if there was a fill color change we look for circular geometries from which we can make
  862. # drill holes for the Excellon file
  863. if current_subpath == 'bezier':
  864. # if there are geometries in the list
  865. if path_geo:
  866. try:
  867. for g in path_geo:
  868. new_el = {}
  869. new_el['clear'] = g
  870. clear_apertures_dict['0']['geometry'].append(new_el)
  871. except TypeError:
  872. new_el = {}
  873. new_el['clear'] = path_geo
  874. clear_apertures_dict['0']['geometry'].append(new_el)
  875. # now that we finished searching for drill holes (this is not very precise because holes in the
  876. # polygon pours may appear as drill too, but .. hey you can't have it all ...) we add
  877. # clear_geometry
  878. try:
  879. for pdf_geo in path_geo:
  880. if isinstance(pdf_geo, MultiPolygon):
  881. for poly in pdf_geo:
  882. new_el = {}
  883. new_el['clear'] = poly
  884. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  885. else:
  886. new_el = {}
  887. new_el['clear'] = pdf_geo
  888. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  889. except KeyError:
  890. # in case there is no stroke width yet therefore no aperture
  891. apertures_dict['0'] = {}
  892. apertures_dict['0']['size'] = applied_size
  893. apertures_dict['0']['type'] = 'C'
  894. apertures_dict['0']['geometry'] = []
  895. for pdf_geo in path_geo:
  896. if isinstance(pdf_geo, MultiPolygon):
  897. for poly in pdf_geo:
  898. new_el = {}
  899. new_el['clear'] = poly
  900. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  901. else:
  902. new_el = {}
  903. new_el['clear'] = pdf_geo
  904. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  905. else:
  906. # else, add the geometry as usual
  907. try:
  908. for pdf_geo in path_geo:
  909. if isinstance(pdf_geo, MultiPolygon):
  910. for poly in pdf_geo:
  911. new_el = {}
  912. new_el['solid'] = poly
  913. new_el['follow'] = poly.exterior
  914. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  915. else:
  916. new_el = {}
  917. new_el['solid'] = pdf_geo
  918. new_el['follow'] = pdf_geo.exterior
  919. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  920. except KeyError:
  921. # in case there is no stroke width yet therefore no aperture
  922. apertures_dict['0'] = {}
  923. apertures_dict['0']['size'] = applied_size
  924. apertures_dict['0']['type'] = 'C'
  925. apertures_dict['0']['geometry'] = []
  926. for pdf_geo in path_geo:
  927. if isinstance(pdf_geo, MultiPolygon):
  928. for poly in pdf_geo:
  929. new_el = {}
  930. new_el['solid'] = poly
  931. new_el['follow'] = poly.exterior
  932. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  933. else:
  934. new_el = {}
  935. new_el['solid'] = pdf_geo
  936. new_el['follow'] = pdf_geo.exterior
  937. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  938. continue
  939. # Fill and Stroke the path
  940. match = self.fill_stroke_path_re.search(pline)
  941. if match:
  942. # scale the size here; some PDF printers apply transformation after the size is declared
  943. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  944. path_geo = []
  945. fill_geo = []
  946. if current_subpath == 'lines':
  947. if path['lines']:
  948. # fill
  949. for subp in path['lines']:
  950. geo = copy(subp)
  951. # close the subpath if it was not closed already
  952. if close_subpath is False:
  953. geo.append(geo[0])
  954. try:
  955. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  956. fill_geo.append(geo_el)
  957. except ValueError:
  958. pass
  959. # stroke
  960. for subp in path['lines']:
  961. geo = copy(subp)
  962. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  963. path_geo.append(geo)
  964. # the path was painted therefore initialize it
  965. path['lines'] = []
  966. else:
  967. # fill
  968. geo = copy(subpath['lines'])
  969. # close the subpath if it was not closed already
  970. if close_subpath is False:
  971. geo.append(start_point)
  972. try:
  973. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  974. fill_geo.append(geo_el)
  975. except ValueError:
  976. pass
  977. # stroke
  978. geo = copy(subpath['lines'])
  979. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  980. path_geo.append(geo)
  981. subpath['lines'] = []
  982. subpath['lines'] = []
  983. if current_subpath == 'bezier':
  984. geo = []
  985. if path['bezier']:
  986. # fill
  987. for subp in path['bezier']:
  988. for b in subp:
  989. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  990. # close the subpath if it was not closed already
  991. if close_subpath is False:
  992. geo.append(geo[0])
  993. try:
  994. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  995. fill_geo.append(geo_el)
  996. except ValueError:
  997. pass
  998. # stroke
  999. for subp in path['bezier']:
  1000. geo = []
  1001. for b in subp:
  1002. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  1003. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1004. path_geo.append(geo)
  1005. # the path was painted therefore initialize it
  1006. path['bezier'] = []
  1007. else:
  1008. # fill
  1009. for b in subpath['bezier']:
  1010. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  1011. if close_subpath is False:
  1012. geo.append(start_point)
  1013. try:
  1014. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  1015. fill_geo.append(geo_el)
  1016. except ValueError:
  1017. pass
  1018. # stroke
  1019. geo = []
  1020. for b in subpath['bezier']:
  1021. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  1022. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1023. path_geo.append(geo)
  1024. subpath['bezier'] = []
  1025. if current_subpath == 'rectangle':
  1026. if path['rectangle']:
  1027. # fill
  1028. for subp in path['rectangle']:
  1029. geo = copy(subp)
  1030. # # close the subpath if it was not closed already
  1031. # if close_subpath is False:
  1032. # geo.append(geo[0])
  1033. try:
  1034. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  1035. fill_geo.append(geo_el)
  1036. except ValueError:
  1037. pass
  1038. # stroke
  1039. for subp in path['rectangle']:
  1040. geo = copy(subp)
  1041. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1042. path_geo.append(geo)
  1043. # the path was painted therefore initialize it
  1044. path['rectangle'] = []
  1045. else:
  1046. # fill
  1047. geo = copy(subpath['rectangle'])
  1048. # # close the subpath if it was not closed already
  1049. # if close_subpath is False:
  1050. # geo.append(start_point)
  1051. try:
  1052. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  1053. fill_geo.append(geo_el)
  1054. except ValueError:
  1055. pass
  1056. # stroke
  1057. geo = copy(subpath['rectangle'])
  1058. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1059. path_geo.append(geo)
  1060. subpath['rectangle'] = []
  1061. # we finished painting and also closed the path if it was the case
  1062. close_subpath = True
  1063. # store the found geometry for stroking the path
  1064. found_aperture = None
  1065. if apertures_dict:
  1066. for apid in apertures_dict:
  1067. # if we already have an aperture with the current size (rounded to 5 decimals)
  1068. if apertures_dict[apid]['size'] == round(applied_size, 5):
  1069. found_aperture = apid
  1070. break
  1071. if found_aperture:
  1072. for pdf_geo in path_geo:
  1073. if isinstance(pdf_geo, MultiPolygon):
  1074. for poly in pdf_geo:
  1075. new_el = {}
  1076. new_el['solid'] = poly
  1077. new_el['follow'] = poly.exterior
  1078. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  1079. else:
  1080. new_el = {}
  1081. new_el['solid'] = pdf_geo
  1082. new_el['follow'] = pdf_geo.exterior
  1083. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  1084. else:
  1085. if str(aperture) in apertures_dict.keys():
  1086. aperture += 1
  1087. apertures_dict[str(aperture)] = {}
  1088. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  1089. apertures_dict[str(aperture)]['type'] = 'C'
  1090. apertures_dict[str(aperture)]['geometry'] = []
  1091. for pdf_geo in path_geo:
  1092. if isinstance(pdf_geo, MultiPolygon):
  1093. for poly in pdf_geo:
  1094. new_el = {}
  1095. new_el['solid'] = poly
  1096. new_el['follow'] = poly.exterior
  1097. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1098. else:
  1099. new_el = {}
  1100. new_el['solid'] = pdf_geo
  1101. new_el['follow'] = pdf_geo.exterior
  1102. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1103. else:
  1104. apertures_dict[str(aperture)] = {}
  1105. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  1106. apertures_dict[str(aperture)]['type'] = 'C'
  1107. apertures_dict[str(aperture)]['geometry'] = []
  1108. for pdf_geo in path_geo:
  1109. if isinstance(pdf_geo, MultiPolygon):
  1110. for poly in pdf_geo:
  1111. new_el = {}
  1112. new_el['solid'] = poly
  1113. new_el['follow'] = poly.exterior
  1114. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1115. else:
  1116. new_el = {}
  1117. new_el['solid'] = pdf_geo
  1118. new_el['follow'] = pdf_geo.exterior
  1119. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1120. # ############################################# ##
  1121. # store the found geometry for filling the path #
  1122. # ############################################# ##
  1123. # in case that a color change to white (transparent) occurred
  1124. if flag_clear_geo is True:
  1125. try:
  1126. for pdf_geo in path_geo:
  1127. if isinstance(pdf_geo, MultiPolygon):
  1128. for poly in fill_geo:
  1129. new_el = {}
  1130. new_el['clear'] = poly
  1131. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1132. else:
  1133. new_el = {}
  1134. new_el['clear'] = pdf_geo
  1135. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1136. except KeyError:
  1137. # in case there is no stroke width yet therefore no aperture
  1138. apertures_dict['0'] = {}
  1139. apertures_dict['0']['size'] = round(applied_size, 5)
  1140. apertures_dict['0']['type'] = 'C'
  1141. apertures_dict['0']['geometry'] = []
  1142. for pdf_geo in fill_geo:
  1143. if isinstance(pdf_geo, MultiPolygon):
  1144. for poly in pdf_geo:
  1145. new_el = {}
  1146. new_el['clear'] = poly
  1147. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1148. else:
  1149. new_el = {}
  1150. new_el['clear'] = pdf_geo
  1151. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1152. else:
  1153. try:
  1154. for pdf_geo in path_geo:
  1155. if isinstance(pdf_geo, MultiPolygon):
  1156. for poly in fill_geo:
  1157. new_el = {}
  1158. new_el['solid'] = poly
  1159. new_el['follow'] = poly.exterior
  1160. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1161. else:
  1162. new_el = {}
  1163. new_el['solid'] = pdf_geo
  1164. new_el['follow'] = pdf_geo.exterior
  1165. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1166. except KeyError:
  1167. # in case there is no stroke width yet therefore no aperture
  1168. apertures_dict['0'] = {}
  1169. apertures_dict['0']['size'] = round(applied_size, 5)
  1170. apertures_dict['0']['type'] = 'C'
  1171. apertures_dict['0']['geometry'] = []
  1172. for pdf_geo in fill_geo:
  1173. if isinstance(pdf_geo, MultiPolygon):
  1174. for poly in pdf_geo:
  1175. new_el = {}
  1176. new_el['solid'] = poly
  1177. new_el['follow'] = poly.exterior
  1178. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1179. else:
  1180. new_el = {}
  1181. new_el['solid'] = pdf_geo
  1182. new_el['follow'] = pdf_geo.exterior
  1183. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1184. continue
  1185. # tidy up. copy the current aperture dict to the object dict but only if it is not empty
  1186. if apertures_dict:
  1187. object_dict[layer_nr] = deepcopy(apertures_dict)
  1188. if clear_apertures_dict['0']['geometry']:
  1189. object_dict[0] = deepcopy(clear_apertures_dict)
  1190. # delete keys (layers) with empty values
  1191. empty_layers = []
  1192. for layer in object_dict:
  1193. if not object_dict[layer]:
  1194. empty_layers.append(layer)
  1195. for x in empty_layers:
  1196. if x in object_dict:
  1197. object_dict.pop(x)
  1198. if self.app.abort_flag:
  1199. # graceful abort requested by the user
  1200. raise grace
  1201. return object_dict
  1202. def bezier_to_points(self, start, c1, c2, stop):
  1203. """
  1204. # Equation Bezier, page 184 PDF 1.4 reference
  1205. # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  1206. # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0
  1207. # in the following equation:
  1208. # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2 + P3*t ** 3
  1209. # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides
  1210. # with the final point P3. Intermediate values of t generate intermediate points along the curve.
  1211. # The curve does not, in general, pass through the two control points P1 and P2
  1212. :return: A list of point coordinates tuples (x, y)
  1213. """
  1214. # here we store the geometric points
  1215. points = []
  1216. nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles))
  1217. for t in nr_points:
  1218. term_p0 = (1 - t) ** 3
  1219. term_p1 = 3 * t * (1 - t) ** 2
  1220. term_p2 = 3 * (1 - t) * t ** 2
  1221. term_p3 = t ** 3
  1222. x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3
  1223. y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3
  1224. points.append([x, y])
  1225. return points
  1226. # def bezier_to_circle(self, path):
  1227. # lst = []
  1228. # for el in range(len(path)):
  1229. # if type(path) is list:
  1230. # for coord in path[el]:
  1231. # lst.append(coord)
  1232. # else:
  1233. # lst.append(el)
  1234. #
  1235. # if lst:
  1236. # minx = min(lst, key=lambda t: t[0])[0]
  1237. # miny = min(lst, key=lambda t: t[1])[1]
  1238. # maxx = max(lst, key=lambda t: t[0])[0]
  1239. # maxy = max(lst, key=lambda t: t[1])[1]
  1240. # center = (maxx-minx, maxy-miny)
  1241. # radius = (maxx-minx) / 2
  1242. # return [center, radius]
  1243. #
  1244. # def circle_to_points(self, center, radius):
  1245. # geo = Point(center).buffer(radius, resolution=self.step_per_circles)
  1246. # return LineString(list(geo.exterior.coords))
  1247. #