ToolPDF.py 65 KB

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