ToolPDF.py 50 KB

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