ToolPDF.py 65 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382
  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:
  169. pass
  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 = self.app.new_object("excellon", outname, obj_init, autoselected=False)
  214. if ret == '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:
  295. pass
  296. self.check_thread.setInterval(check_period)
  297. try:
  298. self.check_thread.timeout.disconnect(self.periodic_check_handler)
  299. except:
  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]) * \
  568. self.point_to_unit_factor * scale_geo[0]
  569. height = (float(match.group(4)) + offset_geo[1]) * \
  570. self.point_to_unit_factor * scale_geo[1]
  571. pt1 = (x, y)
  572. pt2 = (x+width, y)
  573. pt3 = (x+width, y+height)
  574. pt4 = (x, y+height)
  575. subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1]
  576. current_point = pt1
  577. continue
  578. # Detect clipping path set
  579. # ignore this and delete the current subpath
  580. match = self.clip_path_re.search(pline)
  581. if match:
  582. subpath['lines'] = []
  583. subpath['bezier'] = []
  584. subpath['rectangle'] = []
  585. # it means that we've already added the subpath to path and we need to delete it
  586. # clipping path is usually either rectangle or lines
  587. if close_subpath is True:
  588. close_subpath = False
  589. if current_subpath == 'lines':
  590. path['lines'].pop(-1)
  591. if current_subpath == 'rectangle':
  592. path['rectangle'].pop(-1)
  593. continue
  594. # Close SUBPATH
  595. match = self.end_subpath_re.search(pline)
  596. if match:
  597. close_subpath = True
  598. if current_subpath == 'lines':
  599. subpath['lines'].append(start_point)
  600. # since we are closing the subpath add it to the path, a path may have chained subpaths
  601. path['lines'].append(copy(subpath['lines']))
  602. subpath['lines'] = []
  603. elif current_subpath == 'bezier':
  604. # subpath['bezier'].append(start_point)
  605. # since we are closing the subpath add it to the path, a path may have chained subpaths
  606. path['bezier'].append(copy(subpath['bezier']))
  607. subpath['bezier'] = []
  608. elif current_subpath == 'rectangle':
  609. # subpath['rectangle'].append(start_point)
  610. # since we are closing the subpath add it to the path, a path may have chained subpaths
  611. path['rectangle'].append(copy(subpath['rectangle']))
  612. subpath['rectangle'] = []
  613. continue
  614. # PATH PAINTING #
  615. # Detect Stroke width / aperture
  616. match = self.strokewidth_re.search(pline)
  617. if match:
  618. size = float(match.group(1))
  619. continue
  620. # Detect No_Op command, ignore the current subpath
  621. match = self.no_op_re.search(pline)
  622. if match:
  623. subpath['lines'] = []
  624. subpath['bezier'] = []
  625. subpath['rectangle'] = []
  626. continue
  627. # Stroke the path
  628. match = self.stroke_path__re.search(pline)
  629. if match:
  630. # scale the size here; some PDF printers apply transformation after the size is declared
  631. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  632. path_geo = list()
  633. if current_subpath == 'lines':
  634. if path['lines']:
  635. for subp in path['lines']:
  636. geo = copy(subp)
  637. try:
  638. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  639. path_geo.append(geo)
  640. except ValueError:
  641. pass
  642. # the path was painted therefore initialize it
  643. path['lines'] = []
  644. else:
  645. geo = copy(subpath['lines'])
  646. try:
  647. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  648. path_geo.append(geo)
  649. except ValueError:
  650. pass
  651. subpath['lines'] = []
  652. if current_subpath == 'bezier':
  653. if path['bezier']:
  654. for subp in path['bezier']:
  655. geo = []
  656. for b in subp:
  657. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  658. try:
  659. geo = LineString(geo).buffer((float(applied_size) / 2),
  660. resolution=self.step_per_circles)
  661. path_geo.append(geo)
  662. except ValueError:
  663. pass
  664. # the path was painted therefore initialize it
  665. path['bezier'] = []
  666. else:
  667. geo = []
  668. for b in subpath['bezier']:
  669. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  670. try:
  671. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  672. path_geo.append(geo)
  673. except ValueError:
  674. pass
  675. subpath['bezier'] = []
  676. if current_subpath == 'rectangle':
  677. if path['rectangle']:
  678. for subp in path['rectangle']:
  679. geo = copy(subp)
  680. try:
  681. geo = LineString(geo).buffer((float(applied_size) / 2),
  682. resolution=self.step_per_circles)
  683. path_geo.append(geo)
  684. except ValueError:
  685. pass
  686. # the path was painted therefore initialize it
  687. path['rectangle'] = []
  688. else:
  689. geo = copy(subpath['rectangle'])
  690. try:
  691. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  692. path_geo.append(geo)
  693. except ValueError:
  694. pass
  695. subpath['rectangle'] = []
  696. # store the found geometry
  697. found_aperture = None
  698. if apertures_dict:
  699. for apid in apertures_dict:
  700. # if we already have an aperture with the current size (rounded to 5 decimals)
  701. if apertures_dict[apid]['size'] == round(applied_size, 5):
  702. found_aperture = apid
  703. break
  704. if found_aperture:
  705. for pdf_geo in path_geo:
  706. if isinstance(pdf_geo, MultiPolygon):
  707. for poly in pdf_geo:
  708. new_el = dict()
  709. new_el['solid'] = poly
  710. new_el['follow'] = poly.exterior
  711. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  712. else:
  713. new_el = dict()
  714. new_el['solid'] = pdf_geo
  715. new_el['follow'] = pdf_geo.exterior
  716. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  717. found_aperture = None
  718. else:
  719. if str(aperture) in apertures_dict.keys():
  720. aperture += 1
  721. apertures_dict[str(aperture)] = {}
  722. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  723. apertures_dict[str(aperture)]['type'] = 'C'
  724. apertures_dict[str(aperture)]['geometry'] = []
  725. for pdf_geo in path_geo:
  726. if isinstance(pdf_geo, MultiPolygon):
  727. for poly in pdf_geo:
  728. new_el = dict()
  729. new_el['solid'] = poly
  730. new_el['follow'] = poly.exterior
  731. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  732. else:
  733. new_el = dict()
  734. new_el['solid'] = pdf_geo
  735. new_el['follow'] = pdf_geo.exterior
  736. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  737. else:
  738. apertures_dict[str(aperture)] = {}
  739. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  740. apertures_dict[str(aperture)]['type'] = 'C'
  741. apertures_dict[str(aperture)]['geometry'] = []
  742. for pdf_geo in path_geo:
  743. if isinstance(pdf_geo, MultiPolygon):
  744. for poly in pdf_geo:
  745. new_el = dict()
  746. new_el['solid'] = poly
  747. new_el['follow'] = poly.exterior
  748. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  749. else:
  750. new_el = dict()
  751. new_el['solid'] = pdf_geo
  752. new_el['follow'] = pdf_geo.exterior
  753. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  754. continue
  755. # Fill the path
  756. match = self.fill_path_re.search(pline)
  757. if match:
  758. # scale the size here; some PDF printers apply transformation after the size is declared
  759. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  760. path_geo = list()
  761. if current_subpath == 'lines':
  762. if path['lines']:
  763. for subp in path['lines']:
  764. geo = copy(subp)
  765. # close the subpath if it was not closed already
  766. if close_subpath is False:
  767. geo.append(geo[0])
  768. try:
  769. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  770. path_geo.append(geo_el)
  771. except ValueError:
  772. pass
  773. # the path was painted therefore initialize it
  774. path['lines'] = []
  775. else:
  776. geo = copy(subpath['lines'])
  777. # close the subpath if it was not closed already
  778. if close_subpath is False:
  779. geo.append(start_point)
  780. try:
  781. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  782. path_geo.append(geo_el)
  783. except ValueError:
  784. pass
  785. subpath['lines'] = []
  786. if current_subpath == 'bezier':
  787. geo = []
  788. if path['bezier']:
  789. for subp in path['bezier']:
  790. for b in subp:
  791. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  792. # close the subpath if it was not closed already
  793. if close_subpath is False:
  794. geo.append(geo[0])
  795. try:
  796. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  797. path_geo.append(geo_el)
  798. except ValueError:
  799. pass
  800. # the path was painted therefore initialize it
  801. path['bezier'] = []
  802. else:
  803. for b in subpath['bezier']:
  804. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  805. if close_subpath is False:
  806. geo.append(start_point)
  807. try:
  808. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  809. path_geo.append(geo_el)
  810. except ValueError:
  811. pass
  812. subpath['bezier'] = []
  813. if current_subpath == 'rectangle':
  814. if path['rectangle']:
  815. for subp in path['rectangle']:
  816. geo = copy(subp)
  817. # # close the subpath if it was not closed already
  818. # if close_subpath is False and start_point is not None:
  819. # geo.append(start_point)
  820. try:
  821. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  822. path_geo.append(geo_el)
  823. except ValueError:
  824. pass
  825. # the path was painted therefore initialize it
  826. path['rectangle'] = []
  827. else:
  828. geo = copy(subpath['rectangle'])
  829. # # close the subpath if it was not closed already
  830. # if close_subpath is False and start_point is not None:
  831. # geo.append(start_point)
  832. try:
  833. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  834. path_geo.append(geo_el)
  835. except ValueError:
  836. pass
  837. subpath['rectangle'] = []
  838. # we finished painting and also closed the path if it was the case
  839. close_subpath = True
  840. # in case that a color change to white (transparent) occurred
  841. if flag_clear_geo is True:
  842. # if there was a fill color change we look for circular geometries from which we can make
  843. # drill holes for the Excellon file
  844. if current_subpath == 'bezier':
  845. # if there are geometries in the list
  846. if path_geo:
  847. try:
  848. for g in path_geo:
  849. new_el = dict()
  850. new_el['clear'] = g
  851. clear_apertures_dict['0']['geometry'].append(new_el)
  852. except TypeError:
  853. new_el = dict()
  854. new_el['clear'] = path_geo
  855. clear_apertures_dict['0']['geometry'].append(new_el)
  856. # now that we finished searching for drill holes (this is not very precise because holes in the
  857. # polygon pours may appear as drill too, but .. hey you can't have it all ...) we add
  858. # clear_geometry
  859. try:
  860. for pdf_geo in path_geo:
  861. if isinstance(pdf_geo, MultiPolygon):
  862. for poly in pdf_geo:
  863. new_el = dict()
  864. new_el['clear'] = poly
  865. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  866. else:
  867. new_el = dict()
  868. new_el['clear'] = pdf_geo
  869. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  870. except KeyError:
  871. # in case there is no stroke width yet therefore no aperture
  872. apertures_dict['0'] = {}
  873. apertures_dict['0']['size'] = applied_size
  874. apertures_dict['0']['type'] = 'C'
  875. apertures_dict['0']['geometry'] = []
  876. for pdf_geo in path_geo:
  877. if isinstance(pdf_geo, MultiPolygon):
  878. for poly in pdf_geo:
  879. new_el = dict()
  880. new_el['clear'] = poly
  881. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  882. else:
  883. new_el = dict()
  884. new_el['clear'] = pdf_geo
  885. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  886. else:
  887. # else, add the geometry as usual
  888. try:
  889. for pdf_geo in path_geo:
  890. if isinstance(pdf_geo, MultiPolygon):
  891. for poly in pdf_geo:
  892. new_el = dict()
  893. new_el['solid'] = poly
  894. new_el['follow'] = poly.exterior
  895. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  896. else:
  897. new_el = dict()
  898. new_el['solid'] = pdf_geo
  899. new_el['follow'] = pdf_geo.exterior
  900. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  901. except KeyError:
  902. # in case there is no stroke width yet therefore no aperture
  903. apertures_dict['0'] = {}
  904. apertures_dict['0']['size'] = applied_size
  905. apertures_dict['0']['type'] = 'C'
  906. apertures_dict['0']['geometry'] = []
  907. for pdf_geo in path_geo:
  908. if isinstance(pdf_geo, MultiPolygon):
  909. for poly in pdf_geo:
  910. new_el = dict()
  911. new_el['solid'] = poly
  912. new_el['follow'] = poly.exterior
  913. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  914. else:
  915. new_el = dict()
  916. new_el['solid'] = pdf_geo
  917. new_el['follow'] = pdf_geo.exterior
  918. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  919. continue
  920. # Fill and Stroke the path
  921. match = self.fill_stroke_path_re.search(pline)
  922. if match:
  923. # scale the size here; some PDF printers apply transformation after the size is declared
  924. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  925. path_geo = list()
  926. fill_geo = list()
  927. if current_subpath == 'lines':
  928. if path['lines']:
  929. # fill
  930. for subp in path['lines']:
  931. geo = copy(subp)
  932. # close the subpath if it was not closed already
  933. if close_subpath is False:
  934. geo.append(geo[0])
  935. try:
  936. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  937. fill_geo.append(geo_el)
  938. except ValueError:
  939. pass
  940. # stroke
  941. for subp in path['lines']:
  942. geo = copy(subp)
  943. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  944. path_geo.append(geo)
  945. # the path was painted therefore initialize it
  946. path['lines'] = []
  947. else:
  948. # fill
  949. geo = copy(subpath['lines'])
  950. # close the subpath if it was not closed already
  951. if close_subpath is False:
  952. geo.append(start_point)
  953. try:
  954. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  955. fill_geo.append(geo_el)
  956. except ValueError:
  957. pass
  958. # stroke
  959. geo = copy(subpath['lines'])
  960. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  961. path_geo.append(geo)
  962. subpath['lines'] = []
  963. subpath['lines'] = []
  964. if current_subpath == 'bezier':
  965. geo = []
  966. if path['bezier']:
  967. # fill
  968. for subp in path['bezier']:
  969. for b in subp:
  970. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  971. # close the subpath if it was not closed already
  972. if close_subpath is False:
  973. geo.append(geo[0])
  974. try:
  975. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  976. fill_geo.append(geo_el)
  977. except ValueError:
  978. pass
  979. # stroke
  980. for subp in path['bezier']:
  981. geo = []
  982. for b in subp:
  983. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  984. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  985. path_geo.append(geo)
  986. # the path was painted therefore initialize it
  987. path['bezier'] = []
  988. else:
  989. # fill
  990. for b in subpath['bezier']:
  991. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  992. if close_subpath is False:
  993. geo.append(start_point)
  994. try:
  995. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  996. fill_geo.append(geo_el)
  997. except ValueError:
  998. pass
  999. # stroke
  1000. geo = []
  1001. for b in subpath['bezier']:
  1002. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  1003. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1004. path_geo.append(geo)
  1005. subpath['bezier'] = []
  1006. if current_subpath == 'rectangle':
  1007. if path['rectangle']:
  1008. # fill
  1009. for subp in path['rectangle']:
  1010. geo = copy(subp)
  1011. # # close the subpath if it was not closed already
  1012. # if close_subpath is False:
  1013. # geo.append(geo[0])
  1014. try:
  1015. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  1016. fill_geo.append(geo_el)
  1017. except ValueError:
  1018. pass
  1019. # stroke
  1020. for subp in path['rectangle']:
  1021. geo = copy(subp)
  1022. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1023. path_geo.append(geo)
  1024. # the path was painted therefore initialize it
  1025. path['rectangle'] = []
  1026. else:
  1027. # fill
  1028. geo = copy(subpath['rectangle'])
  1029. # # close the subpath if it was not closed already
  1030. # if close_subpath is False:
  1031. # geo.append(start_point)
  1032. try:
  1033. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  1034. fill_geo.append(geo_el)
  1035. except ValueError:
  1036. pass
  1037. # stroke
  1038. geo = copy(subpath['rectangle'])
  1039. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  1040. path_geo.append(geo)
  1041. subpath['rectangle'] = []
  1042. # we finished painting and also closed the path if it was the case
  1043. close_subpath = True
  1044. # store the found geometry for stroking the path
  1045. found_aperture = None
  1046. if apertures_dict:
  1047. for apid in apertures_dict:
  1048. # if we already have an aperture with the current size (rounded to 5 decimals)
  1049. if apertures_dict[apid]['size'] == round(applied_size, 5):
  1050. found_aperture = apid
  1051. break
  1052. if found_aperture:
  1053. for pdf_geo in path_geo:
  1054. if isinstance(pdf_geo, MultiPolygon):
  1055. for poly in pdf_geo:
  1056. new_el = dict()
  1057. new_el['solid'] = poly
  1058. new_el['follow'] = poly.exterior
  1059. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  1060. else:
  1061. new_el = dict()
  1062. new_el['solid'] = pdf_geo
  1063. new_el['follow'] = pdf_geo.exterior
  1064. apertures_dict[copy(found_aperture)]['geometry'].append(deepcopy(new_el))
  1065. found_aperture = None
  1066. else:
  1067. if str(aperture) in apertures_dict.keys():
  1068. aperture += 1
  1069. apertures_dict[str(aperture)] = {}
  1070. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  1071. apertures_dict[str(aperture)]['type'] = 'C'
  1072. apertures_dict[str(aperture)]['geometry'] = []
  1073. for pdf_geo in path_geo:
  1074. if isinstance(pdf_geo, MultiPolygon):
  1075. for poly in pdf_geo:
  1076. new_el = dict()
  1077. new_el['solid'] = poly
  1078. new_el['follow'] = poly.exterior
  1079. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1080. else:
  1081. new_el = dict()
  1082. new_el['solid'] = pdf_geo
  1083. new_el['follow'] = pdf_geo.exterior
  1084. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1085. else:
  1086. apertures_dict[str(aperture)] = {}
  1087. apertures_dict[str(aperture)]['size'] = round(applied_size, 5)
  1088. apertures_dict[str(aperture)]['type'] = 'C'
  1089. apertures_dict[str(aperture)]['geometry'] = []
  1090. for pdf_geo in path_geo:
  1091. if isinstance(pdf_geo, MultiPolygon):
  1092. for poly in pdf_geo:
  1093. new_el = dict()
  1094. new_el['solid'] = poly
  1095. new_el['follow'] = poly.exterior
  1096. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1097. else:
  1098. new_el = dict()
  1099. new_el['solid'] = pdf_geo
  1100. new_el['follow'] = pdf_geo.exterior
  1101. apertures_dict[str(aperture)]['geometry'].append(deepcopy(new_el))
  1102. # ###############################################
  1103. # store the found geometry for filling the path #
  1104. # ###############################################
  1105. # in case that a color change to white (transparent) occurred
  1106. if flag_clear_geo is True:
  1107. try:
  1108. for pdf_geo in path_geo:
  1109. if isinstance(pdf_geo, MultiPolygon):
  1110. for poly in fill_geo:
  1111. new_el = dict()
  1112. new_el['clear'] = poly
  1113. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1114. else:
  1115. new_el = dict()
  1116. new_el['clear'] = pdf_geo
  1117. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1118. except KeyError:
  1119. # in case there is no stroke width yet therefore no aperture
  1120. apertures_dict['0'] = {}
  1121. apertures_dict['0']['size'] = round(applied_size, 5)
  1122. apertures_dict['0']['type'] = 'C'
  1123. apertures_dict['0']['geometry'] = []
  1124. for pdf_geo in fill_geo:
  1125. if isinstance(pdf_geo, MultiPolygon):
  1126. for poly in pdf_geo:
  1127. new_el = dict()
  1128. new_el['clear'] = poly
  1129. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1130. else:
  1131. new_el = dict()
  1132. new_el['clear'] = pdf_geo
  1133. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1134. else:
  1135. try:
  1136. for pdf_geo in path_geo:
  1137. if isinstance(pdf_geo, MultiPolygon):
  1138. for poly in fill_geo:
  1139. new_el = dict()
  1140. new_el['solid'] = poly
  1141. new_el['follow'] = poly.exterior
  1142. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1143. else:
  1144. new_el = dict()
  1145. new_el['solid'] = pdf_geo
  1146. new_el['follow'] = pdf_geo.exterior
  1147. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1148. except KeyError:
  1149. # in case there is no stroke width yet therefore no aperture
  1150. apertures_dict['0'] = {}
  1151. apertures_dict['0']['size'] = round(applied_size, 5)
  1152. apertures_dict['0']['type'] = 'C'
  1153. apertures_dict['0']['geometry'] = []
  1154. for pdf_geo in fill_geo:
  1155. if isinstance(pdf_geo, MultiPolygon):
  1156. for poly in pdf_geo:
  1157. new_el = dict()
  1158. new_el['solid'] = poly
  1159. new_el['follow'] = poly.exterior
  1160. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1161. else:
  1162. new_el = dict()
  1163. new_el['solid'] = pdf_geo
  1164. new_el['follow'] = pdf_geo.exterior
  1165. apertures_dict['0']['geometry'].append(deepcopy(new_el))
  1166. continue
  1167. # tidy up. copy the current aperture dict to the object dict but only if it is not empty
  1168. if apertures_dict:
  1169. object_dict[layer_nr] = deepcopy(apertures_dict)
  1170. if clear_apertures_dict['0']['geometry']:
  1171. object_dict[0] = deepcopy(clear_apertures_dict)
  1172. # delete keys (layers) with empty values
  1173. empty_layers = []
  1174. for layer in object_dict:
  1175. if not object_dict[layer]:
  1176. empty_layers.append(layer)
  1177. for x in empty_layers:
  1178. if x in object_dict:
  1179. object_dict.pop(x)
  1180. return object_dict
  1181. def bezier_to_points(self, start, c1, c2, stop):
  1182. """
  1183. # Equation Bezier, page 184 PDF 1.4 reference
  1184. # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  1185. # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0
  1186. # in the following equation:
  1187. # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2 + P3*t ** 3
  1188. # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides
  1189. # with the final point P3. Intermediate values of t generate intermediate points along the curve.
  1190. # The curve does not, in general, pass through the two control points P1 and P2
  1191. :return: A list of point coordinates tuples (x, y)
  1192. """
  1193. # here we store the geometric points
  1194. points = []
  1195. nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles))
  1196. for t in nr_points:
  1197. term_p0 = (1 - t) ** 3
  1198. term_p1 = 3 * t * (1 - t) ** 2
  1199. term_p2 = 3 * (1 - t) * t ** 2
  1200. term_p3 = t ** 3
  1201. x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3
  1202. y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3
  1203. points.append([x, y])
  1204. return points
  1205. # def bezier_to_circle(self, path):
  1206. # lst = []
  1207. # for el in range(len(path)):
  1208. # if type(path) is list:
  1209. # for coord in path[el]:
  1210. # lst.append(coord)
  1211. # else:
  1212. # lst.append(el)
  1213. #
  1214. # if lst:
  1215. # minx = min(lst, key=lambda t: t[0])[0]
  1216. # miny = min(lst, key=lambda t: t[1])[1]
  1217. # maxx = max(lst, key=lambda t: t[0])[0]
  1218. # maxy = max(lst, key=lambda t: t[1])[1]
  1219. # center = (maxx-minx, maxy-miny)
  1220. # radius = (maxx-minx) / 2
  1221. # return [center, radius]
  1222. #
  1223. # def circle_to_points(self, center, radius):
  1224. # geo = Point(center).buffer(radius, resolution=self.step_per_circles)
  1225. # return LineString(list(geo.exterior.coords))
  1226. #