ToolPDF.py 46 KB

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