ToolPDF.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791
  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. self.geo_buffer = []
  74. self.pdf_parsed = ''
  75. # conversion factor to INCH
  76. self.point_to_unit_factor = 0.01388888888
  77. def run(self, toggle=True):
  78. self.app.report_usage("ToolPDF()")
  79. # init variables for reuse
  80. self.geo_buffer = []
  81. self.pdf_parsed = ''
  82. # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
  83. if self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
  84. # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch = 0.01388888888 inch * 25.4 = 0.35277777778 mm
  85. self.point_to_unit_factor = 0.35277777778
  86. else:
  87. # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch
  88. self.point_to_unit_factor = 0.01388888888
  89. self.set_tool_ui()
  90. self.on_open_pdf_click()
  91. def install(self, icon=None, separator=None, **kwargs):
  92. FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
  93. def set_tool_ui(self):
  94. pass
  95. def on_open_pdf_click(self):
  96. """
  97. File menu callback for opening an PDF file.
  98. :return: None
  99. """
  100. self.app.report_usage("ToolPDF.on_open_pdf_click()")
  101. self.app.log.debug("ToolPDF.on_open_pdf_click()")
  102. _filter_ = "Adobe PDF Files (*.pdf);;" \
  103. "All Files (*.*)"
  104. try:
  105. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
  106. directory=self.app.get_last_folder(),
  107. filter=_filter_)
  108. except TypeError:
  109. filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
  110. if len(filenames) == 0:
  111. self.app.inform.emit(_("[WARNING_NOTCL] Open PDF cancelled."))
  112. else:
  113. for filename in filenames:
  114. if filename != '':
  115. self.app.worker_task.emit({'fcn': self.open_pdf, 'params': [filename]})
  116. def open_pdf(self, filename):
  117. new_name = filename.split('/')[-1].split('\\')[-1]
  118. def obj_init(grb_obj, app_obj):
  119. with open(filename, "rb") as f:
  120. pdf = f.read()
  121. stream_nr = 0
  122. for s in re.findall(self.stream_re, pdf):
  123. stream_nr += 1
  124. log.debug(" PDF STREAM: %d\n" % stream_nr)
  125. s = s.strip(b'\r\n')
  126. try:
  127. self.pdf_parsed += (zlib.decompress(s).decode('UTF-8') + '\r\n')
  128. except Exception as e:
  129. app_obj.log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
  130. ap_dict = self.parse_pdf(pdf_content=self.pdf_parsed)
  131. grb_obj.apertures = deepcopy(ap_dict)
  132. poly_buff = []
  133. for ap in ap_dict:
  134. for k in ap_dict[ap]:
  135. if k == 'solid_geometry':
  136. poly_buff += ap_dict[ap][k]
  137. poly_buff = unary_union(poly_buff)
  138. poly_buff = poly_buff.buffer(0.0000001)
  139. poly_buff = poly_buff.buffer(-0.0000001)
  140. grb_obj.solid_geometry = deepcopy(poly_buff)
  141. with self.app.proc_container.new(_("Opening PDF.")):
  142. ret = self.app.new_object("gerber", new_name, obj_init, autoselected=False)
  143. if ret == 'fail':
  144. self.app.inform.emit(_('[ERROR_NOTCL] Open PDF file failed.'))
  145. return
  146. # Register recent file
  147. self.app.file_opened.emit("gerber", new_name)
  148. # GUI feedback
  149. self.app.inform.emit(_("[success] Opened: %s") % filename)
  150. def parse_pdf(self, pdf_content):
  151. path = dict()
  152. path['lines'] = [] # it's a list of lines subpaths
  153. path['bezier'] = [] # it's a list of bezier arcs subpaths
  154. path['rectangle'] = [] # it's a list of rectangle subpaths
  155. subpath = dict()
  156. subpath['lines'] = [] # it's a list of points
  157. subpath['bezier'] = [] # it's a list of sublists each like this [start, c1, c2, stop]
  158. subpath['rectangle'] = [] # it's a list of sublists of points
  159. # store the start point (when 'm' command is encountered)
  160. current_subpath = None
  161. # set True when 'h' command is encountered (close path)
  162. close_path = False
  163. start_point = None
  164. current_point = None
  165. size = 0
  166. # initial values for the transformations, in case they are not encountered in the PDF file
  167. offset_geo = [0, 0]
  168. scale_geo = [1, 1]
  169. c_offset_f= [0, 0]
  170. c_scale_f = [1, 1]
  171. # initial aperture
  172. aperture = 10
  173. # store the apertures here
  174. apertures_dict = {}
  175. # it seems that first transform apply to the whole PDF; signal here if it's first
  176. first_transform = True
  177. line_nr = 0
  178. lines = pdf_content.splitlines()
  179. for pline in lines:
  180. line_nr += 1
  181. log.debug("line %d: %s" % (line_nr, pline))
  182. # TRANSFORMATIONS DETECTION #
  183. # # Detect Scale transform
  184. # match = self.scale_re.search(pline)
  185. # if match:
  186. # log.debug(
  187. # "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
  188. # if first_transform:
  189. # first_transform = False
  190. # c_scale_f = [float(match.group(1)), float(match.group(2))]
  191. # else:
  192. # scale_geo = [float(match.group(1)), float(match.group(2))]
  193. # continue
  194. # # Detect Offset transform
  195. # match = self.offset_re.search(pline)
  196. # if match:
  197. # log.debug(
  198. # "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
  199. # offset_geo = [float(match.group(1)), float(match.group(2))]
  200. # continue
  201. # Detect combined transformation. Must be always the last from transformations to be checked.
  202. match = self.combined_transform_re.search(pline)
  203. if match:
  204. # transformation = TRANSLATION (OFFSET)
  205. if (float(match.group(2)) == 0 and float(match.group(3)) == 0) and \
  206. (float(match.group(5)) != 0 or float(match.group(6)) != 0):
  207. log.debug(
  208. "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
  209. if first_transform:
  210. c_offset_f = [float(match.group(5)), float(match.group(6))]
  211. else:
  212. offset_geo = [float(match.group(5)), float(match.group(6))]
  213. # transformation = SCALING
  214. if float(match.group(1)) != 1 and float(match.group(4)) != 1:
  215. log.debug(
  216. "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
  217. if first_transform:
  218. c_scale_f = [float(match.group(1)), float(match.group(4))]
  219. else:
  220. scale_geo = [float(match.group(1)), float(match.group(4))]
  221. if first_transform:
  222. first_transform = False
  223. continue
  224. # PATH CONSTRUCTION #
  225. # Start SUBPATH
  226. match = self.start_subpath_re.search(pline)
  227. if match:
  228. # we just started a subpath so we mark it as not closed yet
  229. close_path = False
  230. # init subpaths
  231. subpath['lines'] = []
  232. subpath['bezier'] = []
  233. subpath['rectangle'] = []
  234. # detect start point to move to
  235. x = float(match.group(1)) + offset_geo[0]
  236. y = float(match.group(2)) + offset_geo[1]
  237. pt = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  238. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  239. start_point = pt
  240. # add the start point to subpaths
  241. subpath['lines'].append(start_point)
  242. # subpath['bezier'].append(start_point)
  243. subpath['rectangle'].append(start_point)
  244. current_point = start_point
  245. continue
  246. # Draw Line
  247. match = self.draw_line_re.search(pline)
  248. if match:
  249. current_subpath = 'lines'
  250. x = float(match.group(1)) + offset_geo[0]
  251. y = float(match.group(2)) + offset_geo[1]
  252. pt = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  253. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  254. subpath['lines'].append(pt)
  255. current_point = pt
  256. continue
  257. # Draw Bezier 'c'
  258. match = self.draw_arc_3pt_re.search(pline)
  259. if match:
  260. current_subpath = 'bezier'
  261. start = current_point
  262. x = float(match.group(1)) + offset_geo[0]
  263. y = float(match.group(2)) + offset_geo[1]
  264. c1 = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  265. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  266. x = float(match.group(3)) + offset_geo[0]
  267. y = float(match.group(4)) + offset_geo[1]
  268. c2 = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  269. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  270. x = float(match.group(5)) + offset_geo[0]
  271. y = float(match.group(6)) + offset_geo[1]
  272. stop = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  273. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  274. subpath['bezier'].append([start, c1, c2, stop])
  275. current_point = stop
  276. continue
  277. # Draw Bezier 'v'
  278. match = self.draw_arc_2pt_c1start_re.search(pline)
  279. if match:
  280. current_subpath = 'bezier'
  281. start = current_point
  282. x = float(match.group(1)) + offset_geo[0]
  283. y = float(match.group(2)) + offset_geo[1]
  284. c2 = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  285. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  286. x = float(match.group(3)) + offset_geo[0]
  287. y = float(match.group(4)) + offset_geo[1]
  288. stop = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  289. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  290. subpath['bezier'].append([start, start, c2, stop])
  291. current_point = stop
  292. continue
  293. # Draw Bezier 'y'
  294. match = self.draw_arc_2pt_c2stop_re.search(pline)
  295. if match:
  296. start = current_point
  297. x = float(match.group(1)) + offset_geo[0]
  298. y = float(match.group(2)) + offset_geo[1]
  299. c1 = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  300. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  301. x = float(match.group(3)) + offset_geo[0]
  302. y = float(match.group(4)) + offset_geo[1]
  303. stop = (x * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0],
  304. y * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1])
  305. subpath['bezier'].append([start, c1, stop, stop])
  306. print(subpath['bezier'])
  307. current_point = stop
  308. continue
  309. # Draw RECTANGLE
  310. match = self.rect_re.search(pline)
  311. if match:
  312. current_subpath = 'rectangle'
  313. x = (float(match.group(1)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0] * c_scale_f[0]
  314. y = (float(match.group(2)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1] * c_scale_f[1]
  315. width = (float(match.group(3)) + offset_geo[0]) * \
  316. self.point_to_unit_factor * scale_geo[0] * c_scale_f[0]
  317. height = (float(match.group(4)) + offset_geo[1]) * \
  318. self.point_to_unit_factor * scale_geo[1] * c_scale_f[1]
  319. pt1 = (x, y)
  320. pt2 = (x+width, y)
  321. pt3 = (x+width, y+height)
  322. pt4 = (x, y+height)
  323. # TODO: I'm not sure if rectangles are a subpath in themselves that autoclose
  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 measns 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_path is True:
  337. close_path = 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_path = 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. # flag = 0
  369. #
  370. # if not apertures_dict:
  371. # apertures_dict[str(aperture)] = dict()
  372. # apertures_dict[str(aperture)]['size'] = size
  373. # apertures_dict[str(aperture)]['type'] = 'C'
  374. # apertures_dict[str(aperture)]['solid_geometry'] = []
  375. # else:
  376. # for k in apertures_dict:
  377. # if size == apertures_dict[k]['size']:
  378. # flag = 1
  379. # break
  380. # if flag == 0:
  381. # aperture += 1
  382. # apertures_dict[str(aperture)] = dict()
  383. # apertures_dict[str(aperture)]['size'] = size
  384. # apertures_dict[str(aperture)]['type'] = 'C'
  385. # apertures_dict[str(aperture)]['solid_geometry'] = []
  386. continue
  387. # Detect No_Op command, ignore the current subpath
  388. match = self.no_op_re.search(pline)
  389. if match:
  390. subpath['lines'] = []
  391. subpath['bezier'] = []
  392. subpath['rectangle'] = []
  393. continue
  394. # Stroke the path
  395. match = self.stroke_path__re.search(pline)
  396. if match:
  397. # scale the size here; some PDF printers apply transformation after the size is declared
  398. applied_size = size * scale_geo[0] * c_scale_f[0] * self.point_to_unit_factor
  399. path_geo = list()
  400. if current_subpath == 'lines':
  401. if path['lines']:
  402. for subp in path['lines']:
  403. geo = copy(subp)
  404. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  405. path_geo.append(geo)
  406. # the path was painted therefore initialize it
  407. path['lines'] = []
  408. else:
  409. geo = copy(subpath['lines'])
  410. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  411. path_geo.append(geo)
  412. subpath['lines'] = []
  413. if current_subpath == 'bezier':
  414. if path['bezier']:
  415. for subp in path['bezier']:
  416. geo = []
  417. for b in subp:
  418. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  419. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  420. path_geo.append(geo)
  421. # the path was painted therefore initialize it
  422. path['bezier'] = []
  423. else:
  424. geo = []
  425. for b in subpath['bezier']:
  426. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  427. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  428. path_geo.append(geo)
  429. subpath['bezier'] = []
  430. if current_subpath == 'rectangle':
  431. if path['rectangle']:
  432. for subp in path['rectangle']:
  433. geo = copy(subp)
  434. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  435. path_geo.append(geo)
  436. # the path was painted therefore initialize it
  437. path['rectangle'] = []
  438. else:
  439. geo = copy(subpath['rectangle'])
  440. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  441. path_geo.append(geo)
  442. subpath['rectangle'] = []
  443. try:
  444. apertures_dict[str(aperture)]['solid_geometry'] += path_geo
  445. except KeyError:
  446. # in case there is no stroke width yet therefore no aperture
  447. apertures_dict[str(aperture)] = {}
  448. apertures_dict[str(aperture)]['size'] = applied_size
  449. apertures_dict[str(aperture)]['type'] = 'C'
  450. apertures_dict[str(aperture)]['solid_geometry'] = []
  451. apertures_dict[str(aperture)]['solid_geometry'] += path_geo
  452. continue
  453. # Fill the path
  454. match = self.fill_path_re.search(pline)
  455. if match:
  456. # scale the size here; some PDF printers apply transformation after the size is declared
  457. applied_size = size * scale_geo[0] * c_scale_f[0] * self.point_to_unit_factor
  458. path_geo = list()
  459. if current_subpath == 'lines':
  460. if path['lines']:
  461. for subp in path['lines']:
  462. geo = copy(subp)
  463. # close the subpath if it was not closed already
  464. if close_path is False:
  465. geo.append(geo[0])
  466. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  467. path_geo.append(geo_el)
  468. # the path was painted therefore initialize it
  469. path['lines'] = []
  470. else:
  471. geo = copy(subpath['lines'])
  472. # close the subpath if it was not closed already
  473. if close_path is False:
  474. geo.append(start_point)
  475. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  476. path_geo.append(geo_el)
  477. subpath['lines'] = []
  478. if current_subpath == 'bezier':
  479. geo = []
  480. if path['bezier']:
  481. for subp in path['bezier']:
  482. for b in subp:
  483. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  484. # close the subpath if it was not closed already
  485. if close_path is False:
  486. geo.append(geo[0])
  487. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  488. path_geo.append(geo_el)
  489. # the path was painted therefore initialize it
  490. path['bezier'] = []
  491. else:
  492. for b in subpath['bezier']:
  493. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  494. if close_path is False:
  495. geo.append(start_point)
  496. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  497. path_geo.append(geo_el)
  498. subpath['bezier'] = []
  499. if current_subpath == 'rectangle':
  500. if path['rectangle']:
  501. for subp in path['rectangle']:
  502. geo = copy(subp)
  503. # close the subpath if it was not closed already
  504. if close_path is False:
  505. geo.append(geo[0])
  506. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  507. path_geo.append(geo_el)
  508. # the path was painted therefore initialize it
  509. path['rectangle'] = []
  510. else:
  511. geo = copy(subpath['rectangle'])
  512. # close the subpath if it was not closed already
  513. if close_path is False:
  514. geo.append(start_point)
  515. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  516. path_geo.append(geo_el)
  517. subpath['rectangle'] = []
  518. # we finished painting and also closed the path if it was the case
  519. close_path = True
  520. try:
  521. apertures_dict['0']['solid_geometry'] += path_geo
  522. except KeyError:
  523. # in case there is no stroke width yet therefore no aperture
  524. apertures_dict['0'] = {}
  525. apertures_dict['0']['size'] = applied_size
  526. apertures_dict['0']['type'] = 'C'
  527. apertures_dict['0']['solid_geometry'] = []
  528. apertures_dict['0']['solid_geometry'] += path_geo
  529. continue
  530. # fill and stroke the path
  531. match = self.fill_stroke_path_re.search(pline)
  532. if match:
  533. # scale the size here; some PDF printers apply transformation after the size is declared
  534. applied_size = size * scale_geo[0] * c_scale_f[0] * self.point_to_unit_factor
  535. path_geo = list()
  536. if current_subpath == 'lines':
  537. if path['lines']:
  538. # fill
  539. for subp in path['lines']:
  540. geo = copy(subp)
  541. # close the subpath if it was not closed already
  542. if close_path is False:
  543. geo.append(geo[0])
  544. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  545. path_geo.append(geo_el)
  546. # stroke
  547. for subp in path['lines']:
  548. geo = copy(subp)
  549. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  550. path_geo.append(geo)
  551. # the path was painted therefore initialize it
  552. path['lines'] = []
  553. else:
  554. # fill
  555. geo = copy(subpath['lines'])
  556. # close the subpath if it was not closed already
  557. if close_path is False:
  558. geo.append(start_point)
  559. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  560. path_geo.append(geo_el)
  561. # stroke
  562. geo = copy(subpath['lines'])
  563. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  564. path_geo.append(geo)
  565. subpath['lines'] = []
  566. subpath['lines'] = []
  567. if current_subpath == 'bezier':
  568. geo = []
  569. if path['bezier']:
  570. # fill
  571. for subp in path['bezier']:
  572. for b in subp:
  573. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  574. # close the subpath if it was not closed already
  575. if close_path is False:
  576. geo.append(geo[0])
  577. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  578. path_geo.append(geo_el)
  579. # stroke
  580. for subp in path['bezier']:
  581. geo = []
  582. for b in subp:
  583. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  584. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  585. path_geo.append(geo)
  586. # the path was painted therefore initialize it
  587. path['bezier'] = []
  588. else:
  589. # fill
  590. for b in subpath['bezier']:
  591. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  592. if close_path is False:
  593. geo.append(start_point)
  594. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  595. path_geo.append(geo_el)
  596. # stroke
  597. geo = []
  598. for b in subpath['bezier']:
  599. geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
  600. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  601. path_geo.append(geo)
  602. subpath['bezier'] = []
  603. if current_subpath == 'rectangle':
  604. if path['rectangle']:
  605. # fill
  606. for subp in path['rectangle']:
  607. geo = copy(subp)
  608. # close the subpath if it was not closed already
  609. if close_path is False:
  610. geo.append(geo[0])
  611. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  612. path_geo.append(geo_el)
  613. # stroke
  614. for subp in path['rectangle']:
  615. geo = copy(subp)
  616. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  617. path_geo.append(geo)
  618. # the path was painted therefore initialize it
  619. path['rectangle'] = []
  620. else:
  621. # fill
  622. geo = copy(subpath['rectangle'])
  623. # close the subpath if it was not closed already
  624. if close_path is False:
  625. geo.append(start_point)
  626. geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
  627. path_geo.append(geo_el)
  628. # stroke
  629. geo = copy(subpath['rectangle'])
  630. geo = LineString(geo).buffer((float(applied_size) / 2), resolution=self.step_per_circles)
  631. path_geo.append(geo)
  632. subpath['rectangle'] = []
  633. # we finished painting and also closed the path if it was the case
  634. close_path = True
  635. try:
  636. apertures_dict['0']['solid_geometry'] += path_geo
  637. except KeyError:
  638. # in case there is no stroke width yet therefore no aperture
  639. apertures_dict['0'] = {}
  640. apertures_dict['0']['size'] = applied_size
  641. apertures_dict['0']['type'] = 'C'
  642. apertures_dict['0']['solid_geometry'] = []
  643. apertures_dict['0']['solid_geometry'] += path_geo
  644. continue
  645. return apertures_dict
  646. def bezier_to_points(self, start, c1, c2, stop):
  647. """
  648. # Equation Bezier, page 184 PDF 1.4 reference
  649. # https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
  650. # Given the coordinates of the four points, the curve is generated by varying the parameter t from 0.0 to 1.0
  651. # in the following equation:
  652. # R(t) = P0*(1 - t) ** 3 + P1*3*t*(1 - t) ** 2 + P2 * 3*(1 - t) * t ** 2 + P3*t ** 3
  653. # When t = 0.0, the value from the function coincides with the current point P0; when t = 1.0, R(t) coincides
  654. # with the final point P3. Intermediate values of t generate intermediate points along the curve.
  655. # The curve does not, in general, pass through the two control points P1 and P2
  656. :return: LineString geometry
  657. """
  658. # here we store the geometric points
  659. points = []
  660. nr_points = np.arange(0.0, 1.0, (1 / self.step_per_circles))
  661. for t in nr_points:
  662. term_p0 = (1 - t) ** 3
  663. term_p1 = 3 * t * (1 - t) ** 2
  664. term_p2 = 3 * (1 - t) * t ** 2
  665. term_p3 = t ** 3
  666. x = start[0] * term_p0 + c1[0] * term_p1 + c2[0] * term_p2 + stop[0] * term_p3
  667. y = start[1] * term_p0 + c1[1] * term_p1 + c2[1] * term_p2 + stop[1] * term_p3
  668. points.append([x, y])
  669. return points
  670. # def bezier_to_circle(self, path):
  671. # lst = []
  672. # for el in range(len(path)):
  673. # if type(path) is list:
  674. # for coord in path[el]:
  675. # lst.append(coord)
  676. # else:
  677. # lst.append(el)
  678. #
  679. # if lst:
  680. # minx = min(lst, key=lambda t: t[0])[0]
  681. # miny = min(lst, key=lambda t: t[1])[1]
  682. # maxx = max(lst, key=lambda t: t[0])[0]
  683. # maxy = max(lst, key=lambda t: t[1])[1]
  684. # center = (maxx-minx, maxy-miny)
  685. # radius = (maxx-minx) / 2
  686. # return [center, radius]
  687. #
  688. # def circle_to_points(self, center, radius):
  689. # geo = Point(center).buffer(radius, resolution=self.step_per_circles)
  690. # return LineString(list(geo.exterior.coords))
  691. #