ParsePDF.py 50 KB

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