ToolPDF.py 65 KB

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