ParsePDF.py 52 KB

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