ToolPDF.py 38 KB

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