ToolQRCode.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 10/24/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtCore
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCDoubleSpinner
  10. from flatcamParsers.ParseSVG import *
  11. from shapely.geometry import Point
  12. from shapely.geometry.base import *
  13. from shapely.ops import unary_union
  14. from shapely.affinity import translate
  15. from io import StringIO, BytesIO
  16. from collections import Iterable
  17. import logging
  18. import qrcode
  19. import qrcode.image.svg
  20. from lxml import etree as ET
  21. from copy import copy, deepcopy
  22. from numpy import Inf
  23. import gettext
  24. import FlatCAMTranslation as fcTranslate
  25. import builtins
  26. fcTranslate.apply_language('strings')
  27. if '_' not in builtins.__dict__:
  28. _ = gettext.gettext
  29. log = logging.getLogger('base')
  30. class QRCode(FlatCAMTool):
  31. toolName = _("QRCode Tool")
  32. def __init__(self, app):
  33. FlatCAMTool.__init__(self, app)
  34. self.app = app
  35. self.canvas = self.app.plotcanvas
  36. self.decimals = 4
  37. self.units = ''
  38. # ## Title
  39. title_label = QtWidgets.QLabel("%s" % self.toolName)
  40. title_label.setStyleSheet("""
  41. QLabel
  42. {
  43. font-size: 16px;
  44. font-weight: bold;
  45. }
  46. """)
  47. self.layout.addWidget(title_label)
  48. self.layout.addWidget(QtWidgets.QLabel(''))
  49. # ## Grid Layout
  50. i_grid_lay = QtWidgets.QGridLayout()
  51. self.layout.addLayout(i_grid_lay)
  52. i_grid_lay.setColumnStretch(0, 0)
  53. i_grid_lay.setColumnStretch(1, 1)
  54. self.grb_object_combo = QtWidgets.QComboBox()
  55. self.grb_object_combo.setModel(self.app.collection)
  56. self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  57. self.grb_object_combo.setCurrentIndex(1)
  58. self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  59. self.grbobj_label.setToolTip(
  60. _("Gerber Object to which the QRCode will be added.")
  61. )
  62. i_grid_lay.addWidget(self.grbobj_label, 0, 0)
  63. i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2)
  64. i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
  65. # ## Grid Layout
  66. grid_lay = QtWidgets.QGridLayout()
  67. self.layout.addLayout(grid_lay)
  68. grid_lay.setColumnStretch(0, 0)
  69. grid_lay.setColumnStretch(1, 1)
  70. self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('QRCode Parameters'))
  71. self.qrcode_label.setToolTip(
  72. _("Contain the expected calibration points and the\n"
  73. "ones measured.")
  74. )
  75. grid_lay.addWidget(self.qrcode_label, 0, 0, 1, 2)
  76. # VERSION #
  77. self.version_label = QtWidgets.QLabel('%s:' % _("Version"))
  78. self.version_label.setToolTip(
  79. _("QRCode version can have values from 1 (21x21 boxes)\n"
  80. "to 40 (177x177 boxes).")
  81. )
  82. self.version_entry = FCSpinner()
  83. self.version_entry.set_range(1, 40)
  84. self.version_entry.setWrapping(True)
  85. grid_lay.addWidget(self.version_label, 1, 0)
  86. grid_lay.addWidget(self.version_entry, 1, 1)
  87. # ERROR CORRECTION #
  88. self.error_label = QtWidgets.QLabel('%s:' % _("Error correction"))
  89. self.error_label.setToolTip(
  90. _("Parameter that controls the error correction used for the QR Code.\n"
  91. "L = maximum 7% errors can be corrected\n"
  92. "M = maximum 15% errors can be corrected\n"
  93. "Q = maximum 25% errors can be corrected\n"
  94. "H = maximum 30% errors can be corrected.")
  95. )
  96. self.error_radio = RadioSet([{'label': 'L', 'value': 'L'},
  97. {'label': 'M', 'value': 'M'},
  98. {'label': 'Q', 'value': 'Q'},
  99. {'label': 'H', 'value': 'H'}])
  100. self.error_radio.setToolTip(
  101. _("Parameter that controls the error correction used for the QR Code.\n"
  102. "L = maximum 7% errors can be corrected\n"
  103. "M = maximum 15% errors can be corrected\n"
  104. "Q = maximum 25% errors can be corrected\n"
  105. "H = maximum 30% errors can be corrected.")
  106. )
  107. grid_lay.addWidget(self.error_label, 2, 0)
  108. grid_lay.addWidget(self.error_radio, 2, 1)
  109. # BOX SIZE #
  110. self.bsize_label = QtWidgets.QLabel('%s:' % _("Box Size"))
  111. self.bsize_label.setToolTip(
  112. _("Box size control the overall size of the QRcode\n"
  113. "by adjusting the size of each box in the code.")
  114. )
  115. self.bsize_entry = FCSpinner()
  116. self.bsize_entry.set_range(1, 9999)
  117. self.bsize_entry.setWrapping(True)
  118. grid_lay.addWidget(self.bsize_label, 3, 0)
  119. grid_lay.addWidget(self.bsize_entry, 3, 1)
  120. # BORDER SIZE #
  121. self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size"))
  122. self.border_size_label.setToolTip(
  123. _("Size of the QRCode border. How many boxes thick is the border.\n"
  124. "Default value is 4.")
  125. )
  126. self.border_size_entry = FCSpinner()
  127. self.border_size_entry.set_range(1, 9999)
  128. self.border_size_entry.setWrapping(True)
  129. self.border_size_entry.set_value(4)
  130. grid_lay.addWidget(self.border_size_label, 4, 0)
  131. grid_lay.addWidget(self.border_size_entry, 4, 1)
  132. # Text box
  133. self.text_label = QtWidgets.QLabel('%s:' % _("QRCode Data"))
  134. self.text_label.setToolTip(
  135. _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
  136. )
  137. self.text_data = FCTextArea()
  138. self.text_data.setPlaceholderText(
  139. _("Add here the text to be included in the QRData...")
  140. )
  141. grid_lay.addWidget(self.text_label, 5, 0)
  142. grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
  143. # POLARITY CHOICE #
  144. self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
  145. self.pol_label.setToolTip(
  146. _("Parameter that controls the error correction used for the QR Code.\n"
  147. "L = maximum 7% errors can be corrected\n"
  148. "M = maximum 15% errors can be corrected\n"
  149. "Q = maximum 25% errors can be corrected\n"
  150. "H = maximum 30% errors can be corrected.")
  151. )
  152. self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
  153. {'label': _('Positive'), 'value': 'pos'}])
  154. self.error_radio.setToolTip(
  155. _("Choose the type of QRCode to be created.\n"
  156. "If added on a Silkscreen Gerber you may add\n"
  157. "it as positive. If you add it to a Copper\n"
  158. "Gerber then perhaps you can add it as positive.")
  159. )
  160. grid_lay.addWidget(self.pol_label, 7, 0)
  161. grid_lay.addWidget(self.pol_radio, 7, 1)
  162. # BOUNDARY THICKNESS #
  163. self.boundary_label = QtWidgets.QLabel('%s:' % _("Boundary Thickness"))
  164. self.boundary_label.setToolTip(
  165. _("The width of the clearance around the QRCode.")
  166. )
  167. self.boundary_entry = FCDoubleSpinner()
  168. self.boundary_entry.set_range(0.0, 9999.9999)
  169. self.boundary_entry.set_precision(self.decimals)
  170. self.boundary_entry.setWrapping(True)
  171. grid_lay.addWidget(self.boundary_label, 8, 0)
  172. grid_lay.addWidget(self.boundary_entry, 8, 1)
  173. # ## Create QRCode
  174. self.qrcode_button = QtWidgets.QPushButton(_("Create QRCode"))
  175. self.qrcode_button.setToolTip(
  176. _("Create the QRCode object.")
  177. )
  178. grid_lay.addWidget(self.qrcode_button, 9, 0, 1, 2)
  179. grid_lay.addWidget(QtWidgets.QLabel(''), 10, 0)
  180. self.layout.addStretch()
  181. self.grb_object = None
  182. self.origin = (0, 0)
  183. self.mm = None
  184. self.mr = None
  185. self.kr = None
  186. self.shapes = self.app.move_tool.sel_shapes
  187. self.qrcode_geometry = list()
  188. self.qrcode_utility_geometry = list()
  189. def run(self, toggle=True):
  190. self.app.report_usage("QRCode()")
  191. if toggle:
  192. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  193. if self.app.ui.splitter.sizes()[0] == 0:
  194. self.app.ui.splitter.setSizes([1, 1])
  195. else:
  196. try:
  197. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  198. # if tab is populated with the tool but it does not have the focus, focus on it
  199. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  200. # focus on Tool Tab
  201. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  202. else:
  203. self.app.ui.splitter.setSizes([0, 1])
  204. except AttributeError:
  205. pass
  206. else:
  207. if self.app.ui.splitter.sizes()[0] == 0:
  208. self.app.ui.splitter.setSizes([1, 1])
  209. FlatCAMTool.run(self)
  210. self.set_tool_ui()
  211. self.app.ui.notebook.setTabText(2, _("QRCode Tool"))
  212. def install(self, icon=None, separator=None, **kwargs):
  213. FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
  214. def set_tool_ui(self):
  215. self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value()
  216. self.version_entry.set_value(1)
  217. self.error_radio.set_value('M')
  218. self.bsize_entry.set_value(3)
  219. self.border_size_entry.set_value(4)
  220. self.pol_radio.set_value('pos')
  221. # Signals #
  222. self.qrcode_button.clicked.connect(self.execute)
  223. def execute(self):
  224. text_data = self.text_data.get_value()
  225. if text_data == '':
  226. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
  227. return 'fail'
  228. error_code = {
  229. 'L': qrcode.constants.ERROR_CORRECT_L,
  230. 'M': qrcode.constants.ERROR_CORRECT_M,
  231. 'Q': qrcode.constants.ERROR_CORRECT_Q,
  232. 'H': qrcode.constants.ERROR_CORRECT_H
  233. }[self.error_radio.get_value()]
  234. qr = qrcode.QRCode(
  235. version=self.version_entry.get_value(),
  236. error_correction=error_code,
  237. box_size=self.bsize_entry.get_value(),
  238. border=self.border_size_entry.get_value(),
  239. image_factory=qrcode.image.svg.SvgFragmentImage
  240. )
  241. qr.add_data(text_data)
  242. qr.make()
  243. svg_file = BytesIO()
  244. img = qr.make_image()
  245. img.save(svg_file)
  246. svg_text = StringIO(svg_file.getvalue().decode('UTF-8'))
  247. svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units)
  248. self.qrcode_geometry = deepcopy(svg_geometry)
  249. svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001)
  250. self.qrcode_utility_geometry = svg_geometry
  251. # if we have an object selected then we can safely activate the mouse events
  252. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  253. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  254. self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release)
  255. selection_index = self.grb_object_combo.currentIndex()
  256. model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
  257. try:
  258. self.grb_object = model_index.internalPointer().obj
  259. except Exception as e:
  260. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  261. return 'fail'
  262. self.app.inform.emit(_("Click on the Destination point ..."))
  263. def make(self, pos):
  264. if self.app.is_legacy is False:
  265. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  266. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  267. self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release)
  268. else:
  269. self.app.plotcanvas.graph_event_disconnect(self.mm)
  270. self.app.plotcanvas.graph_event_disconnect(self.mr)
  271. self.app.plotcanvas.graph_event_disconnect(self.kr)
  272. # delete the utility geometry
  273. self.delete_utility_geo()
  274. # add the svg geometry to the selected Gerber object solid_geometry and in obj.apertures, apid = 0
  275. if not isinstance(self.grb_object.solid_geometry, Iterable):
  276. self.grb_object.solid_geometry = list(self.grb_object.solid_geometry)
  277. # I use the utility geometry (self.qrcode_utility_geometry) because it is already buffered
  278. geo_list = self.grb_object.solid_geometry
  279. if isinstance(self.grb_object.solid_geometry, MultiPolygon):
  280. geo_list = list(self.grb_object.solid_geometry.geoms)
  281. try:
  282. for geo in self.qrcode_utility_geometry:
  283. geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1]))
  284. except TypeError:
  285. geo_list.append(translate(self.qrcode_utility_geometry, xoff=pos[0], yoff=pos[1]))
  286. self.grb_object.solid_geometry = deepcopy(geo_list)
  287. box_size = float(self.bsize_entry.get_value()) / 10.0
  288. sort_apid = list()
  289. new_apid = '10'
  290. if self.grb_object.apertures:
  291. for k, v in list(self.grb_object.apertures.items()):
  292. sort_apid.append(int(k))
  293. sorted_apertures = sorted(sort_apid)
  294. new_apid = str(max(sorted_apertures) + 1)
  295. if new_apid not in self.grb_object.apertures:
  296. self.grb_object.apertures[new_apid] = dict()
  297. self.grb_object.apertures[new_apid]['geometry'] = list()
  298. self.grb_object.apertures[new_apid]['type'] = 'R'
  299. self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size)
  300. self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size)
  301. self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2))
  302. try:
  303. a, b, c, d = self.grb_object.bounds()
  304. self.grb_object.options['xmin'] = a
  305. self.grb_object.options['ymin'] = b
  306. self.grb_object.options['xmax'] = c
  307. self.grb_object.options['ymax'] = d
  308. except Exception as e:
  309. log.debug("QRCode.make() bounds error --> %s" % str(e))
  310. try:
  311. for geo in self.qrcode_geometry:
  312. geo_elem = dict()
  313. geo_elem['solid'] = translate(geo, xoff=pos[0], yoff=pos[1])
  314. geo_elem['follow'] = translate(geo.centroid, xoff=pos[0], yoff=pos[1])
  315. self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
  316. except TypeError:
  317. geo_elem = dict()
  318. geo_elem['solid'] = self.qrcode_geometry
  319. self.grb_object.apertures['0']['geometry'].append(deepcopy(geo_elem))
  320. # update the source file with the new geometry:
  321. self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None,
  322. local_use=self.grb_object, use_thread=False)
  323. self.replot()
  324. def draw_utility_geo(self, pos):
  325. face = '#0000FF' + str(hex(int(0.2 * 255)))[2:]
  326. outline = '#0000FFAF'
  327. offset_geo = list()
  328. try:
  329. for poly in self.qrcode_utility_geometry:
  330. offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1]))
  331. for geo_int in poly.interiors:
  332. offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
  333. except TypeError:
  334. offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1]))
  335. for geo_int in self.qrcode_utility_geometry.interiors:
  336. offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
  337. for shape in offset_geo:
  338. self.shapes.add(shape, color=outline, face_color=face, update=True, layer=0, tolerance=None)
  339. if self.app.is_legacy is True:
  340. self.shapes.redraw()
  341. def delete_utility_geo(self):
  342. self.shapes.clear()
  343. self.shapes.redraw()
  344. def on_mouse_move(self, event):
  345. if self.app.is_legacy is False:
  346. event_pos = event.pos
  347. else:
  348. event_pos = (event.xdata, event.ydata)
  349. try:
  350. x = float(event_pos[0])
  351. y = float(event_pos[1])
  352. except TypeError:
  353. return
  354. pos_canvas = self.app.plotcanvas.translate_coords((x, y))
  355. # if GRID is active we need to get the snapped positions
  356. if self.app.grid_status() == True:
  357. pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  358. else:
  359. pos = pos_canvas
  360. dx = pos[0] - self.origin[0]
  361. dy = pos[1] - self.origin[1]
  362. # delete the utility geometry
  363. self.delete_utility_geo()
  364. self.draw_utility_geo((dx, dy))
  365. def on_mouse_release(self, event):
  366. # mouse click will be accepted only if the left button is clicked
  367. # this is necessary because right mouse click and middle mouse click
  368. # are used for panning on the canvas
  369. if self.app.is_legacy is False:
  370. event_pos = event.pos
  371. else:
  372. event_pos = (event.xdata, event.ydata)
  373. if event.button == 1:
  374. pos_canvas = self.app.plotcanvas.translate_coords(event_pos)
  375. self.delete_utility_geo()
  376. # if GRID is active we need to get the snapped positions
  377. if self.app.grid_status() == True:
  378. pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  379. else:
  380. pos = pos_canvas
  381. dx = pos[0] - self.origin[0]
  382. dy = pos[1] - self.origin[1]
  383. self.make(pos=(dx, dy))
  384. def on_key_release(self, event):
  385. pass
  386. def convert_svg_to_geo(self, filename, object_type=None, flip=True, units='MM'):
  387. """
  388. Convert shapes from an SVG file into a geometry list.
  389. :param filename: A String Stream file.
  390. :param object_type: parameter passed further along. What kind the object will receive the SVG geometry
  391. :param flip: Flip the vertically.
  392. :type flip: bool
  393. :param units: FlatCAM units
  394. :return: None
  395. """
  396. # Parse into list of shapely objects
  397. svg_tree = ET.parse(filename)
  398. svg_root = svg_tree.getroot()
  399. # Change origin to bottom left
  400. # h = float(svg_root.get('height'))
  401. # w = float(svg_root.get('width'))
  402. h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
  403. geos = getsvggeo(svg_root, object_type)
  404. if flip:
  405. geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
  406. # flatten the svg geometry for the case when the QRCode SVG is added into a Gerber object
  407. solid_geometry = list(self.flatten_list(geos))
  408. geos_text = getsvgtext(svg_root, object_type, units=units)
  409. if geos_text is not None:
  410. geos_text_f = []
  411. if flip:
  412. # Change origin to bottom left
  413. for i in geos_text:
  414. _, minimy, _, maximy = i.bounds
  415. h2 = (maximy - minimy) * 0.5
  416. geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
  417. if geos_text_f:
  418. solid_geometry += geos_text_f
  419. return solid_geometry
  420. def flatten_list(self, list):
  421. for item in list:
  422. if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
  423. yield from self.flatten_list(item)
  424. else:
  425. yield item
  426. def replot(self):
  427. obj = self.grb_object
  428. def worker_task():
  429. with self.app.proc_container.new('%s...' % _("Plotting")):
  430. obj.plot()
  431. self.app.worker_task.emit({'fcn': worker_task, 'params': []})