ToolPDF.py 38 KB

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