ToolPDF.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://flatcam.org #
  4. # File Author: Marius Adrian Stanciu (c) #
  5. # Date: 3/10/2019 #
  6. # MIT Licence #
  7. ############################################################
  8. from FlatCAMTool import FlatCAMTool
  9. from shapely.geometry import Point, Polygon, LineString
  10. from shapely.ops import cascaded_union, unary_union
  11. from FlatCAMObj import *
  12. import math
  13. from copy import copy, deepcopy
  14. import numpy as np
  15. import zlib
  16. import re
  17. import gettext
  18. import FlatCAMTranslation as fcTranslate
  19. import builtins
  20. fcTranslate.apply_language('strings')
  21. if '_' not in builtins.__dict__:
  22. _ = gettext.gettext
  23. class ToolPDF(FlatCAMTool):
  24. """
  25. Parse a PDF file.
  26. Reference here: https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  27. Return a list of geometries
  28. """
  29. toolName = _("PDF Import Tool")
  30. def __init__(self, app):
  31. FlatCAMTool.__init__(self, app)
  32. self.app = app
  33. self.step_per_circles = self.app.defaults["gerber_circle_steps"]
  34. self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
  35. # detect 're' command
  36. self.rect_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*re$')
  37. # detect 'm' command
  38. self.start_subpath_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sm$')
  39. # detect 'l' command
  40. self.draw_line_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\sl')
  41. # detect 'c' command
  42. self.draw_arc_3pt_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)'
  43. r'\s(-?\d+\.?\d*)\s*c$')
  44. # detect 'v' command
  45. self.draw_arc_2pt_c1start_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*v$')
  46. # detect 'y' command
  47. self.draw_arc_2pt_c2stop_re = re.compile(r'^(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*y$')
  48. # detect 'h' command
  49. self.end_subpath_re = re.compile(r'^h$')
  50. # detect 'w' command
  51. self.strokewidth_re = re.compile(r'^(\d+\.?\d*)\s*w$')
  52. # detect 'S' command
  53. self.stroke_path__re = re.compile(r'^S\s?[Q]?$')
  54. # detect 's' command
  55. self.close_stroke_path__re = re.compile(r'^s$')
  56. # detect 'f' or 'f*' command
  57. self.fill_path_re = re.compile(r'^[f|F][*]?$')
  58. # detect 'B' or 'B*' command
  59. self.fill_stroke_path_re = re.compile(r'^B[*]?$')
  60. # detect 'b' or 'b*' command
  61. self.close_fill_stroke_path_re = re.compile(r'^b[*]?$')
  62. # detect 'n'
  63. self.no_op_re = re.compile(r'^n$')
  64. # detect offset transformation. Pattern: (1) (0) (0) (1) (x) (y)
  65. # self.offset_re = re.compile(r'^1\.?0*\s0?\.?0*\s0?\.?0*\s1\.?0*\s(-?\d+\.?\d*)\s(-?\d+\.?\d*)\s*cm$')
  66. # detect scale transformation. Pattern: (factor_x) (0) (0) (factor_y) (0) (0)
  67. # self.scale_re = re.compile(r'^q? (-?\d+\.?\d*) 0\.?0* 0\.?0* (-?\d+\.?\d*) 0\.?0* 0\.?0*\s+cm$')
  68. # detect combined transformation. Should always be the last
  69. self.combined_transform_re = re.compile(r'^(q)?\s*(-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) (-?\d+\.?\d*) '
  70. r'(-?\d+\.?\d*) (-?\d+\.?\d*)\s+cm$')
  71. # detect clipping path
  72. self.clip_path_re = re.compile(r'^W[*]? n?$')
  73. # detect save graphic state in graphic stack
  74. self.save_gs_re = re.compile(r'^q.*?$')
  75. # detect restore graphic state from graphic stack
  76. self.restore_gs_re = re.compile(r'^Q.*$')
  77. # graphic stack where we save parameters like transformation, line_width
  78. self.gs = dict()
  79. # each element is a list composed of sublist elements
  80. # (each sublist has 2 lists each having 2 elements: first is offset like:
  81. # offset_geo = [off_x, off_y], second element is scale list with 2 elements, like: scale_geo = [sc_x, sc_yy])
  82. self.gs['transform'] = []
  83. self.gs['line_width'] = [] # each element is a float
  84. self.geo_buffer = []
  85. self.pdf_parsed = ''
  86. # conversion factor to INCH
  87. self.point_to_unit_factor = 0.01388888888
  88. def run(self, toggle=True):
  89. self.app.report_usage("ToolPDF()")
  90. # init variables for reuse
  91. self.geo_buffer = []
  92. self.pdf_parsed = ''
  93. # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
  94. if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  95. # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm
  96. self.point_to_unit_factor = 0.35277777778
  97. else:
  98. # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch
  99. self.point_to_unit_factor = 0.01388888888
  100. self.set_tool_ui()
  101. self.on_open_pdf_click()
  102. def install(self, icon=None, separator=None, **kwargs):
  103. FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
  104. def set_tool_ui(self):
  105. pass
  106. def on_open_pdf_click(self):
  107. """
  108. File menu callback for opening an PDF file.
  109. :return: None
  110. """
  111. self.app.report_usage("ToolPDF.on_open_pdf_click()")
  112. self.app.log.debug("ToolPDF.on_open_pdf_click()")
  113. _filter_ = "Adobe PDF Files (*.pdf);;" \
  114. "All Files (*.*)"
  115. try:
  116. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
  117. directory=self.app.get_last_folder(),
  118. filter=_filter_)
  119. except TypeError:
  120. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
  121. if len(filenames) == 0:
  122. self.app.inform.emit(_("[WARNING_NOTCL] Open PDF cancelled."))
  123. else:
  124. for filename in filenames:
  125. if filename != '':
  126. self.app.worker_task.emit({'fcn': self.open_pdf, 'params': [filename]})
  127. def open_pdf(self, filename):
  128. new_name = filename.split('/')[-1].split('\\')[-1]
  129. def obj_init(grb_obj, app_obj):
  130. with open(filename, "rb") as f:
  131. pdf = f.read()
  132. stream_nr = 0
  133. for s in re.findall(self.stream_re, pdf):
  134. stream_nr += 1
  135. log.debug(" PDF STREAM: %d\n" % stream_nr)
  136. s = s.strip(b'\r\n')
  137. try:
  138. self.pdf_parsed += (zlib.decompress(s).decode('UTF-8') + '\r\n')
  139. except Exception as e:
  140. app_obj.log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
  141. ap_dict = self.parse_pdf(pdf_content=self.pdf_parsed)
  142. grb_obj.apertures = deepcopy(ap_dict)
  143. poly_buff = []
  144. for ap in ap_dict:
  145. for k in ap_dict[ap]:
  146. if k == 'solid_geometry':
  147. poly_buff += ap_dict[ap][k]
  148. poly_buff = unary_union(poly_buff)
  149. poly_buff = poly_buff.buffer(0.0000001)
  150. poly_buff = poly_buff.buffer(-0.0000001)
  151. grb_obj.solid_geometry = deepcopy(poly_buff)
  152. with self.app.proc_container.new(_("Opening PDF.")):
  153. ret = self.app.new_object("gerber", new_name, obj_init, autoselected=False)
  154. if ret == 'fail':
  155. self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
  156. return
  157. # Register recent file
  158. self.app.file_opened.emit("gerber", new_name)
  159. # GUI feedback
  160. self.app.inform.emit(_("[success] Opened: %s") % filename)
  161. def parse_pdf(self, pdf_content):
  162. path = dict()
  163. path['lines'] = [] # it's a list of lines subpaths
  164. path['bezier'] = [] # it's a list of bezier arcs subpaths
  165. path['rectangle'] = [] # it's a list of rectangle subpaths
  166. subpath = dict()
  167. subpath['lines'] = [] # it's a list of points
  168. subpath['bezier'] = [] # it's a list of sublists each like this [start, c1, c2, stop]
  169. subpath['rectangle'] = [] # it's a list of sublists of points
  170. # store the start point (when 'm' command is encountered)
  171. current_subpath = None
  172. # set True when 'h' command is encountered (close subpath)
  173. close_subpath = False
  174. start_point = None
  175. current_point = None
  176. size = 0
  177. # initial values for the transformations, in case they are not encountered in the PDF file
  178. offset_geo = [0, 0]
  179. scale_geo = [1, 1]
  180. # initial aperture
  181. aperture = 10
  182. # store the apertures here
  183. apertures_dict = {}
  184. line_nr = 0
  185. lines = pdf_content.splitlines()
  186. for pline in lines:
  187. line_nr += 1
  188. # log.debug("line %d: %s" % (line_nr, pline))
  189. # TRANSFORMATIONS DETECTION #
  190. # Detect combined transformation.
  191. match = self.combined_transform_re.search(pline)
  192. if match:
  193. # detect save graphic stack event
  194. # sometimes they combine save_to_graphics_stack with the transformation on the same line
  195. if match.group(1) == 'q':
  196. log.debug(
  197. "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
  198. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  199. self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
  200. self.gs['line_width'].append(deepcopy(size))
  201. # transformation = TRANSLATION (OFFSET)
  202. if (float(match.group(3)) == 0 and float(match.group(4)) == 0) and \
  203. (float(match.group(6)) != 0 or float(match.group(7)) != 0):
  204. log.debug(
  205. "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
  206. offset_geo[0] += float(match.group(6))
  207. offset_geo[1] += float(match.group(7))
  208. # log.debug("Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
  209. # transformation = SCALING
  210. if float(match.group(2)) != 1 and float(match.group(5)) != 1:
  211. log.debug(
  212. "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
  213. scale_geo[0] *= float(match.group(2))
  214. scale_geo[1] *= float(match.group(5))
  215. # log.debug("Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
  216. continue
  217. # detect save graphic stack event
  218. match = self.save_gs_re.search(pline)
  219. if match:
  220. log.debug(
  221. "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
  222. (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
  223. self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
  224. self.gs['line_width'].append(deepcopy(size))
  225. # detect restore from graphic stack event
  226. match = self.restore_gs_re.search(pline)
  227. if match:
  228. log.debug(
  229. "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> %s" % (line_nr, pline))
  230. try:
  231. restored_transform = self.gs['transform'].pop(-1)
  232. offset_geo = restored_transform[0]
  233. scale_geo = restored_transform[1]
  234. except IndexError:
  235. # nothing to remove
  236. log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
  237. pass
  238. try:
  239. size = self.gs['line_width'].pop(-1)
  240. except IndexError:
  241. log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
  242. # nothing to remove
  243. pass
  244. # log.debug("Restored Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
  245. # log.debug("Restored Scale= [%f, %f]" % (scale_geo[0], scale_geo[1]))
  246. # PATH CONSTRUCTION #
  247. # Start SUBPATH
  248. match = self.start_subpath_re.search(pline)
  249. if match:
  250. # we just started a subpath so we mark it as not closed yet
  251. close_subpath = False
  252. # init subpaths
  253. subpath['lines'] = []
  254. subpath['bezier'] = []
  255. subpath['rectangle'] = []
  256. # detect start point to move to
  257. x = float(match.group(1)) + offset_geo[0]
  258. y = float(match.group(2)) + offset_geo[1]
  259. pt = (x * self.point_to_unit_factor * scale_geo[0],
  260. y * self.point_to_unit_factor * scale_geo[1])
  261. start_point = pt
  262. # add the start point to subpaths
  263. subpath['lines'].append(start_point)
  264. # subpath['bezier'].append(start_point)
  265. subpath['rectangle'].append(start_point)
  266. current_point = start_point
  267. continue
  268. # Draw Line
  269. match = self.draw_line_re.search(pline)
  270. if match:
  271. current_subpath = 'lines'
  272. x = float(match.group(1)) + offset_geo[0]
  273. y = float(match.group(2)) + offset_geo[1]
  274. pt = (x * self.point_to_unit_factor * scale_geo[0],
  275. y * self.point_to_unit_factor * scale_geo[1])
  276. subpath['lines'].append(pt)
  277. current_point = pt
  278. continue
  279. # Draw Bezier 'c'
  280. match = self.draw_arc_3pt_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. c1 = (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. c2 = (x * self.point_to_unit_factor * scale_geo[0],
  291. y * self.point_to_unit_factor * scale_geo[1])
  292. x = float(match.group(5)) + offset_geo[0]
  293. y = float(match.group(6)) + offset_geo[1]
  294. stop = (x * self.point_to_unit_factor * scale_geo[0],
  295. y * self.point_to_unit_factor * scale_geo[1])
  296. subpath['bezier'].append([start, c1, c2, stop])
  297. current_point = stop
  298. continue
  299. # Draw Bezier 'v'
  300. match = self.draw_arc_2pt_c1start_re.search(pline)
  301. if match:
  302. current_subpath = 'bezier'
  303. start = current_point
  304. x = float(match.group(1)) + offset_geo[0]
  305. y = float(match.group(2)) + offset_geo[1]
  306. c2 = (x * self.point_to_unit_factor * scale_geo[0],
  307. y * self.point_to_unit_factor * scale_geo[1])
  308. x = float(match.group(3)) + offset_geo[0]
  309. y = float(match.group(4)) + offset_geo[1]
  310. stop = (x * self.point_to_unit_factor * scale_geo[0],
  311. y * self.point_to_unit_factor * scale_geo[1])
  312. subpath['bezier'].append([start, start, c2, stop])
  313. current_point = stop
  314. continue
  315. # Draw Bezier 'y'
  316. match = self.draw_arc_2pt_c2stop_re.search(pline)
  317. if match:
  318. start = current_point
  319. x = float(match.group(1)) + offset_geo[0]
  320. y = float(match.group(2)) + offset_geo[1]
  321. c1 = (x * self.point_to_unit_factor * scale_geo[0],
  322. y * self.point_to_unit_factor * scale_geo[1])
  323. x = float(match.group(3)) + offset_geo[0]
  324. y = float(match.group(4)) + offset_geo[1]
  325. stop = (x * self.point_to_unit_factor * scale_geo[0],
  326. y * self.point_to_unit_factor * scale_geo[1])
  327. subpath['bezier'].append([start, c1, stop, stop])
  328. print(subpath['bezier'])
  329. current_point = stop
  330. continue
  331. # Draw Rectangle 're
  332. match = self.rect_re.search(pline)
  333. if match:
  334. current_subpath = 'rectangle'
  335. x = (float(match.group(1)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0]
  336. y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1]
  337. width = (float(match.group(3)) + offset_geo[0]) * \
  338. self.point_to_unit_factor * scale_geo[0]
  339. height = (float(match.group(4)) + offset_geo[1]) * \
  340. self.point_to_unit_factor * scale_geo[1]
  341. pt1 = (x, y)
  342. pt2 = (x+width, y)
  343. pt3 = (x+width, y+height)
  344. pt4 = (x, y+height)
  345. # TODO: I'm not sure if rectangles are a type of subpath that close by itself
  346. subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1]
  347. current_point = pt1
  348. continue
  349. # Detect clipping path set
  350. # ignore this and delete the current subpath
  351. match = self.clip_path_re.search(pline)
  352. if match:
  353. subpath['lines'] = []
  354. subpath['bezier'] = []
  355. subpath['rectangle'] = []
  356. # it measns that we've already added the subpath to path and we need to delete it
  357. # clipping path is usually either rectangle or lines
  358. if close_subpath is True:
  359. close_subpath = False
  360. if current_subpath == 'lines':
  361. path['lines'].pop(-1)
  362. if current_subpath == 'rectangle':
  363. path['rectangle'].pop(-1)
  364. continue
  365. # Close SUBPATH
  366. match = self.end_subpath_re.search(pline)
  367. if match:
  368. close_subpath = True
  369. if current_subpath == 'lines':
  370. subpath['lines'].append(start_point)
  371. # since we are closing the subpath add it to the path, a path may have chained subpaths
  372. path['lines'].append(copy(subpath['lines']))
  373. subpath['lines'] = []
  374. elif current_subpath == 'bezier':
  375. # subpath['bezier'].append(start_point)
  376. # since we are closing the subpath add it to the path, a path may have chained subpaths
  377. path['bezier'].append(copy(subpath['bezier']))
  378. subpath['bezier'] = []
  379. elif current_subpath == 'rectangle':
  380. subpath['rectangle'].append(start_point)
  381. # since we are closing the subpath add it to the path, a path may have chained subpaths
  382. path['rectangle'].append(copy(subpath['rectangle']))
  383. subpath['rectangle'] = []
  384. continue
  385. # PATH PAINTING #
  386. # Detect Stroke width / aperture
  387. match = self.strokewidth_re.search(pline)
  388. if match:
  389. size = float(match.group(1))
  390. continue
  391. # Detect No_Op command, ignore the current subpath
  392. match = self.no_op_re.search(pline)
  393. if match:
  394. subpath['lines'] = []
  395. subpath['bezier'] = []
  396. subpath['rectangle'] = []
  397. continue
  398. # Stroke the path
  399. match = self.stroke_path__re.search(pline)
  400. if match:
  401. # scale the size here; some PDF printers apply transformation after the size is declared
  402. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  403. path_geo = list()
  404. if current_subpath == 'lines':
  405. if path['lines']:
  406. for subp in path['lines']:
  407. geo = copy(subp)
  408. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  409. path_geo.append(geo)
  410. # the path was painted therefore initialize it
  411. path['lines'] = []
  412. else:
  413. geo = copy(subpath['lines'])
  414. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  415. path_geo.append(geo)
  416. subpath['lines'] = []
  417. if current_subpath == 'bezier':
  418. if path['bezier']:
  419. for subp in path['bezier']:
  420. geo = []
  421. for b in subp:
  422. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  423. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  424. path_geo.append(geo)
  425. # the path was painted therefore initialize it
  426. path['bezier'] = []
  427. else:
  428. geo = []
  429. for b in subpath['bezier']:
  430. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  431. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  432. path_geo.append(geo)
  433. subpath['bezier'] = []
  434. if current_subpath == 'rectangle':
  435. if path['rectangle']:
  436. for subp in path['rectangle']:
  437. geo = copy(subp)
  438. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  439. path_geo.append(geo)
  440. # the path was painted therefore initialize it
  441. path['rectangle'] = []
  442. else:
  443. geo = copy(subpath['rectangle'])
  444. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  445. path_geo.append(geo)
  446. subpath['rectangle'] = []
  447. try:
  448. apertures_dict[str(aperture)]['solid_geometry'] += path_geo
  449. except KeyError:
  450. # in case there is no stroke width yet therefore no aperture
  451. apertures_dict[str(aperture)] = {}
  452. apertures_dict[str(aperture)]['size'] = applied_size
  453. apertures_dict[str(aperture)]['type'] = 'C'
  454. apertures_dict[str(aperture)]['solid_geometry'] = []
  455. apertures_dict[str(aperture)]['solid_geometry'] += path_geo
  456. continue
  457. # Fill the path
  458. match = self.fill_path_re.search(pline)
  459. if match:
  460. # scale the size here; some PDF printers apply transformation after the size is declared
  461. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  462. path_geo = list()
  463. if current_subpath == 'lines':
  464. if path['lines']:
  465. for subp in path['lines']:
  466. geo = copy(subp)
  467. # close the subpath if it was not closed already
  468. if close_subpath is False:
  469. geo.append(geo[0])
  470. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  471. path_geo.append(geo_el)
  472. # the path was painted therefore initialize it
  473. path['lines'] = []
  474. else:
  475. geo = copy(subpath['lines'])
  476. # close the subpath if it was not closed already
  477. if close_subpath is False:
  478. geo.append(start_point)
  479. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  480. path_geo.append(geo_el)
  481. subpath['lines'] = []
  482. if current_subpath == 'bezier':
  483. geo = []
  484. if path['bezier']:
  485. for subp in path['bezier']:
  486. for b in subp:
  487. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  488. # close the subpath if it was not closed already
  489. if close_subpath is False:
  490. geo.append(geo[0])
  491. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  492. path_geo.append(geo_el)
  493. # the path was painted therefore initialize it
  494. path['bezier'] = []
  495. else:
  496. for b in subpath['bezier']:
  497. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  498. if close_subpath is False:
  499. geo.append(start_point)
  500. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  501. path_geo.append(geo_el)
  502. subpath['bezier'] = []
  503. if current_subpath == 'rectangle':
  504. if path['rectangle']:
  505. for subp in path['rectangle']:
  506. geo = copy(subp)
  507. # close the subpath if it was not closed already
  508. if close_subpath is False:
  509. geo.append(geo[0])
  510. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  511. path_geo.append(geo_el)
  512. # the path was painted therefore initialize it
  513. path['rectangle'] = []
  514. else:
  515. geo = copy(subpath['rectangle'])
  516. # close the subpath if it was not closed already
  517. if close_subpath is False:
  518. geo.append(start_point)
  519. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  520. path_geo.append(geo_el)
  521. subpath['rectangle'] = []
  522. # we finished painting and also closed the path if it was the case
  523. close_subpath = True
  524. try:
  525. apertures_dict['0']['solid_geometry'] += path_geo
  526. except KeyError:
  527. # in case there is no stroke width yet therefore no aperture
  528. apertures_dict['0'] = {}
  529. apertures_dict['0']['size'] = applied_size
  530. apertures_dict['0']['type'] = 'C'
  531. apertures_dict['0']['solid_geometry'] = []
  532. apertures_dict['0']['solid_geometry'] += path_geo
  533. continue
  534. # fill and stroke the path
  535. match = self.fill_stroke_path_re.search(pline)
  536. if match:
  537. # scale the size here; some PDF printers apply transformation after the size is declared
  538. applied_size = size * scale_geo[0] * self.point_to_unit_factor
  539. path_geo = list()
  540. if current_subpath == 'lines':
  541. if path['lines']:
  542. # fill
  543. for subp in path['lines']:
  544. geo = copy(subp)
  545. # close the subpath if it was not closed already
  546. if close_subpath is False:
  547. geo.append(geo[0])
  548. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  549. path_geo.append(geo_el)
  550. # stroke
  551. for subp in path['lines']:
  552. geo = copy(subp)
  553. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  554. path_geo.append(geo)
  555. # the path was painted therefore initialize it
  556. path['lines'] = []
  557. else:
  558. # fill
  559. geo = copy(subpath['lines'])
  560. # close the subpath if it was not closed already
  561. if close_subpath is False:
  562. geo.append(start_point)
  563. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  564. path_geo.append(geo_el)
  565. # stroke
  566. geo = copy(subpath['lines'])
  567. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  568. path_geo.append(geo)
  569. subpath['lines'] = []
  570. subpath['lines'] = []
  571. if current_subpath == 'bezier':
  572. geo = []
  573. if path['bezier']:
  574. # fill
  575. for subp in path['bezier']:
  576. for b in subp:
  577. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  578. # close the subpath if it was not closed already
  579. if close_subpath is False:
  580. geo.append(geo[0])
  581. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  582. path_geo.append(geo_el)
  583. # stroke
  584. for subp in path['bezier']:
  585. geo = []
  586. for b in subp:
  587. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  588. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  589. path_geo.append(geo)
  590. # the path was painted therefore initialize it
  591. path['bezier'] = []
  592. else:
  593. # fill
  594. for b in subpath['bezier']:
  595. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  596. if close_subpath is False:
  597. geo.append(start_point)
  598. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  599. path_geo.append(geo_el)
  600. # stroke
  601. geo = []
  602. for b in subpath['bezier']:
  603. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  604. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  605. path_geo.append(geo)
  606. subpath['bezier'] = []
  607. if current_subpath == 'rectangle':
  608. if path['rectangle']:
  609. # fill
  610. for subp in path['rectangle']:
  611. geo = copy(subp)
  612. # close the subpath if it was not closed already
  613. if close_subpath is False:
  614. geo.append(geo[0])
  615. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  616. path_geo.append(geo_el)
  617. # stroke
  618. for subp in path['rectangle']:
  619. geo = copy(subp)
  620. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  621. path_geo.append(geo)
  622. # the path was painted therefore initialize it
  623. path['rectangle'] = []
  624. else:
  625. # fill
  626. geo = copy(subpath['rectangle'])
  627. # close the subpath if it was not closed already
  628. if close_subpath is False:
  629. geo.append(start_point)
  630. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  631. path_geo.append(geo_el)
  632. # stroke
  633. geo = copy(subpath['rectangle'])
  634. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  635. path_geo.append(geo)
  636. subpath['rectangle'] = []
  637. # we finished painting and also closed the path if it was the case
  638. close_subpath = True
  639. try:
  640. apertures_dict['0']['solid_geometry'] += path_geo
  641. except KeyError:
  642. # in case there is no stroke width yet therefore no aperture
  643. apertures_dict['0'] = {}
  644. apertures_dict['0']['size'] = applied_size
  645. apertures_dict['0']['type'] = 'C'
  646. apertures_dict['0']['solid_geometry'] = []
  647. apertures_dict['0']['solid_geometry'] += path_geo
  648. continue
  649. return apertures_dict
  650. def bezier_to_points(self, start, c1, c2, stop):
  651. """
  652. # Equation Bezier, page 184 PDF 1.4 reference
  653. # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  654. # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0
  655. # in the following equation:
  656. # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2 + P3*t ** 3
  657. # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides
  658. # with the final point P3. Intermediate values of t generate intermediate points along the curve.
  659. # The curve does not, in general, pass through the two control points P1 and P2
  660. :return: LineString geometry
  661. """
  662. # here we store the geometric points
  663. points = []
  664. nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles))
  665. for t in nr_points:
  666. term_p0 = (1 - t) ** 3
  667. term_p1 = 3 * t * (1 - t) ** 2
  668. term_p2 = 3 * (1 - t) * t ** 2
  669. term_p3 = t ** 3
  670. x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3
  671. y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3
  672. points.append([x, y])
  673. return points
  674. # def bezier_to_circle(self, path):
  675. # lst = []
  676. # for el in range(len(path)):
  677. # if type(path) is list:
  678. # for coord in path[el]:
  679. # lst.append(coord)
  680. # else:
  681. # lst.append(el)
  682. #
  683. # if lst:
  684. # minx = min(lst, key=lambda t: t[0])[0]
  685. # miny = min(lst, key=lambda t: t[1])[1]
  686. # maxx = max(lst, key=lambda t: t[0])[0]
  687. # maxy = max(lst, key=lambda t: t[1])[1]
  688. # center = (maxx-minx, maxy-miny)
  689. # radius = (maxx-minx) / 2
  690. # return [center, radius]
  691. #
  692. # def circle_to_points(self, center, radius):
  693. # geo = Point(center).buffer(radius, resolution=self.step_per_circles)
  694. # return LineString(list(geo.exterior.coords))
  695. #