ToolQRCode.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887
  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, QtGui
  8. from PyQt5.QtCore import Qt
  9. from FlatCAMTool import FlatCAMTool
  10. from flatcamGUI.GUIElements import RadioSet, FCTextArea, FCSpinner, FCEntry, FCCheckBox
  11. from flatcamParsers.ParseSVG import *
  12. from shapely.geometry.base import *
  13. from shapely.ops import unary_union
  14. from shapely.affinity import translate
  15. from shapely.geometry import box
  16. from io import StringIO, BytesIO
  17. from collections import Iterable
  18. import logging
  19. from copy import deepcopy
  20. import qrcode
  21. import qrcode.image.svg
  22. import qrcode.image.pil
  23. from lxml import etree as ET
  24. import gettext
  25. import FlatCAMTranslation as fcTranslate
  26. import builtins
  27. fcTranslate.apply_language('strings')
  28. if '_' not in builtins.__dict__:
  29. _ = gettext.gettext
  30. log = logging.getLogger('base')
  31. class QRCode(FlatCAMTool):
  32. toolName = _("QRCode Tool")
  33. def __init__(self, app):
  34. FlatCAMTool.__init__(self, app)
  35. self.app = app
  36. self.canvas = self.app.plotcanvas
  37. self.decimals = self.app.decimals
  38. self.units = ''
  39. # ## Title
  40. title_label = QtWidgets.QLabel("%s" % self.toolName)
  41. title_label.setStyleSheet("""
  42. QLabel
  43. {
  44. font-size: 16px;
  45. font-weight: bold;
  46. }
  47. """)
  48. self.layout.addWidget(title_label)
  49. self.layout.addWidget(QtWidgets.QLabel(''))
  50. # ## Grid Layout
  51. i_grid_lay = QtWidgets.QGridLayout()
  52. self.layout.addLayout(i_grid_lay)
  53. i_grid_lay.setColumnStretch(0, 0)
  54. i_grid_lay.setColumnStretch(1, 1)
  55. self.grb_object_combo = QtWidgets.QComboBox()
  56. self.grb_object_combo.setModel(self.app.collection)
  57. self.grb_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  58. self.grb_object_combo.setCurrentIndex(1)
  59. self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
  60. self.grbobj_label.setToolTip(
  61. _("Gerber Object to which the QRCode will be added.")
  62. )
  63. i_grid_lay.addWidget(self.grbobj_label, 0, 0)
  64. i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2)
  65. i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
  66. # ## Grid Layout
  67. grid_lay = QtWidgets.QGridLayout()
  68. self.layout.addLayout(grid_lay)
  69. grid_lay.setColumnStretch(0, 0)
  70. grid_lay.setColumnStretch(1, 1)
  71. self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('QRCode Parameters'))
  72. self.qrcode_label.setToolTip(
  73. _("The parameters used to shape the QRCode.")
  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(callback=self.confirmation_message_int)
  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(callback=self.confirmation_message_int)
  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. The width of the clearance around the QRCode.")
  125. )
  126. self.border_size_entry = FCSpinner(callback=self.confirmation_message_int)
  127. self.border_size_entry.set_range(1, 9999)
  128. self.border_size_entry.setWrapping(True)
  129. grid_lay.addWidget(self.border_size_label, 4, 0)
  130. grid_lay.addWidget(self.border_size_entry, 4, 1)
  131. # Text box
  132. self.text_label = QtWidgets.QLabel('%s:' % _("QRCode Data"))
  133. self.text_label.setToolTip(
  134. _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
  135. )
  136. self.text_data = FCTextArea()
  137. self.text_data.setPlaceholderText(
  138. _("Add here the text to be included in the QRCode...")
  139. )
  140. grid_lay.addWidget(self.text_label, 5, 0)
  141. grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
  142. # POLARITY CHOICE #
  143. self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
  144. self.pol_label.setToolTip(
  145. _("Choose the polarity of the QRCode.\n"
  146. "It can be drawn in a negative way (squares are clear)\n"
  147. "or in a positive way (squares are opaque).")
  148. )
  149. self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
  150. {'label': _('Positive'), 'value': 'pos'}])
  151. self.pol_radio.setToolTip(
  152. _("Choose the type of QRCode to be created.\n"
  153. "If added on a Silkscreen Gerber file the QRCode may\n"
  154. "be added as positive. If it is added to a Copper Gerber\n"
  155. "file then perhaps the QRCode can be added as negative.")
  156. )
  157. grid_lay.addWidget(self.pol_label, 7, 0)
  158. grid_lay.addWidget(self.pol_radio, 7, 1)
  159. # BOUNDING BOX TYPE #
  160. self.bb_label = QtWidgets.QLabel('%s:' % _("Bounding Box"))
  161. self.bb_label.setToolTip(
  162. _("The bounding box, meaning the empty space that surrounds\n"
  163. "the QRCode geometry, can have a rounded or a square shape.")
  164. )
  165. self.bb_radio = RadioSet([{'label': _('Rounded'), 'value': 'r'},
  166. {'label': _('Square'), 'value': 's'}])
  167. self.bb_radio.setToolTip(
  168. _("The bounding box, meaning the empty space that surrounds\n"
  169. "the QRCode geometry, can have a rounded or a square shape.")
  170. )
  171. grid_lay.addWidget(self.bb_label, 8, 0)
  172. grid_lay.addWidget(self.bb_radio, 8, 1)
  173. # Export QRCode
  174. self.export_cb = FCCheckBox(_("Export QRCode"))
  175. self.export_cb.setToolTip(
  176. _("Show a set of controls allowing to export the QRCode\n"
  177. "to a SVG file or an PNG file.")
  178. )
  179. grid_lay.addWidget(self.export_cb, 9, 0, 1, 2)
  180. # this way I can hide/show the frame
  181. self.export_frame = QtWidgets.QFrame()
  182. self.export_frame.setContentsMargins(0, 0, 0, 0)
  183. self.layout.addWidget(self.export_frame)
  184. self.export_lay = QtWidgets.QGridLayout()
  185. self.export_lay.setContentsMargins(0, 0, 0, 0)
  186. self.export_frame.setLayout(self.export_lay)
  187. self.export_lay.setColumnStretch(0, 0)
  188. self.export_lay.setColumnStretch(1, 1)
  189. # default is hidden
  190. self.export_frame.hide()
  191. # FILL COLOR #
  192. self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill Color'))
  193. self.fill_color_label.setToolTip(
  194. _("Set the QRCode fill color (squares color).")
  195. )
  196. self.fill_color_entry = FCEntry()
  197. self.fill_color_button = QtWidgets.QPushButton()
  198. self.fill_color_button.setFixedSize(15, 15)
  199. fill_lay_child = QtWidgets.QHBoxLayout()
  200. fill_lay_child.setContentsMargins(0, 0, 0, 0)
  201. fill_lay_child.addWidget(self.fill_color_entry)
  202. fill_lay_child.addWidget(self.fill_color_button, alignment=Qt.AlignRight)
  203. fill_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
  204. fill_color_widget = QtWidgets.QWidget()
  205. fill_color_widget.setLayout(fill_lay_child)
  206. self.export_lay.addWidget(self.fill_color_label, 0, 0)
  207. self.export_lay.addWidget(fill_color_widget, 0, 1)
  208. self.transparent_cb = FCCheckBox(_("Transparent back color"))
  209. self.export_lay.addWidget(self.transparent_cb, 1, 0, 1, 2)
  210. # BACK COLOR #
  211. self.back_color_label = QtWidgets.QLabel('%s:' % _('Back Color'))
  212. self.back_color_label.setToolTip(
  213. _("Set the QRCode background color.")
  214. )
  215. self.back_color_entry = FCEntry()
  216. self.back_color_button = QtWidgets.QPushButton()
  217. self.back_color_button.setFixedSize(15, 15)
  218. back_lay_child = QtWidgets.QHBoxLayout()
  219. back_lay_child.setContentsMargins(0, 0, 0, 0)
  220. back_lay_child.addWidget(self.back_color_entry)
  221. back_lay_child.addWidget(self.back_color_button, alignment=Qt.AlignRight)
  222. back_lay_child.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
  223. back_color_widget = QtWidgets.QWidget()
  224. back_color_widget.setLayout(back_lay_child)
  225. self.export_lay.addWidget(self.back_color_label, 2, 0)
  226. self.export_lay.addWidget(back_color_widget, 2, 1)
  227. # ## Export QRCode as SVG image
  228. self.export_svg_button = QtWidgets.QPushButton(_("Export QRCode SVG"))
  229. self.export_svg_button.setToolTip(
  230. _("Export a SVG file with the QRCode content.")
  231. )
  232. self.export_svg_button.setStyleSheet("""
  233. QPushButton
  234. {
  235. font-weight: bold;
  236. }
  237. """)
  238. self.export_lay.addWidget(self.export_svg_button, 3, 0, 1, 2)
  239. # ## Export QRCode as PNG image
  240. self.export_png_button = QtWidgets.QPushButton(_("Export QRCode PNG"))
  241. self.export_png_button.setToolTip(
  242. _("Export a PNG image file with the QRCode content.")
  243. )
  244. self.export_png_button.setStyleSheet("""
  245. QPushButton
  246. {
  247. font-weight: bold;
  248. }
  249. """)
  250. self.export_lay.addWidget(self.export_png_button, 4, 0, 1, 2)
  251. # ## Insert QRCode
  252. self.qrcode_button = QtWidgets.QPushButton(_("Insert QRCode"))
  253. self.qrcode_button.setToolTip(
  254. _("Create the QRCode object.")
  255. )
  256. self.qrcode_button.setStyleSheet("""
  257. QPushButton
  258. {
  259. font-weight: bold;
  260. }
  261. """)
  262. self.layout.addWidget(self.qrcode_button)
  263. self.layout.addStretch()
  264. # ## Reset Tool
  265. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  266. self.reset_button.setToolTip(
  267. _("Will reset the tool parameters.")
  268. )
  269. self.reset_button.setStyleSheet("""
  270. QPushButton
  271. {
  272. font-weight: bold;
  273. }
  274. """)
  275. self.layout.addWidget(self.reset_button)
  276. self.grb_object = None
  277. self.box_poly = None
  278. self.proc = None
  279. self.origin = (0, 0)
  280. self.mm = None
  281. self.mr = None
  282. self.kr = None
  283. self.shapes = self.app.move_tool.sel_shapes
  284. self.qrcode_geometry = MultiPolygon()
  285. self.qrcode_utility_geometry = MultiPolygon()
  286. self.old_back_color = ''
  287. # Signals #
  288. self.qrcode_button.clicked.connect(self.execute)
  289. self.export_cb.stateChanged.connect(self.on_export_frame)
  290. self.export_png_button.clicked.connect(self.export_png_file)
  291. self.export_svg_button.clicked.connect(self.export_svg_file)
  292. self.fill_color_entry.editingFinished.connect(self.on_qrcode_fill_color_entry)
  293. self.fill_color_button.clicked.connect(self.on_qrcode_fill_color_button)
  294. self.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry)
  295. self.back_color_button.clicked.connect(self.on_qrcode_back_color_button)
  296. self.transparent_cb.stateChanged.connect(self.on_transparent_back_color)
  297. self.reset_button.clicked.connect(self.set_tool_ui)
  298. def run(self, toggle=True):
  299. self.app.report_usage("QRCode()")
  300. if toggle:
  301. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  302. if self.app.ui.splitter.sizes()[0] == 0:
  303. self.app.ui.splitter.setSizes([1, 1])
  304. else:
  305. try:
  306. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  307. # if tab is populated with the tool but it does not have the focus, focus on it
  308. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  309. # focus on Tool Tab
  310. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  311. else:
  312. self.app.ui.splitter.setSizes([0, 1])
  313. except AttributeError:
  314. pass
  315. else:
  316. if self.app.ui.splitter.sizes()[0] == 0:
  317. self.app.ui.splitter.setSizes([1, 1])
  318. FlatCAMTool.run(self)
  319. self.set_tool_ui()
  320. self.app.ui.notebook.setTabText(2, _("QRCode Tool"))
  321. def install(self, icon=None, separator=None, **kwargs):
  322. FlatCAMTool.install(self, icon, separator, shortcut='ALT+Q', **kwargs)
  323. def set_tool_ui(self):
  324. self.units = self.app.defaults['units']
  325. self.border_size_entry.set_value(4)
  326. self.version_entry.set_value(int(self.app.defaults["tools_qrcode_version"]))
  327. self.error_radio.set_value(self.app.defaults["tools_qrcode_error"])
  328. self.bsize_entry.set_value(int(self.app.defaults["tools_qrcode_box_size"]))
  329. self.border_size_entry.set_value(int(self.app.defaults["tools_qrcode_border_size"]))
  330. self.pol_radio.set_value(self.app.defaults["tools_qrcode_polarity"])
  331. self.bb_radio.set_value(self.app.defaults["tools_qrcode_rounded"])
  332. self.text_data.set_value(self.app.defaults["tools_qrcode_qrdata"])
  333. self.fill_color_entry.set_value(self.app.defaults['tools_qrcode_fill_color'])
  334. self.fill_color_button.setStyleSheet("background-color:%s" %
  335. str(self.app.defaults['tools_qrcode_fill_color'])[:7])
  336. self.back_color_entry.set_value(self.app.defaults['tools_qrcode_back_color'])
  337. self.back_color_button.setStyleSheet("background-color:%s" %
  338. str(self.app.defaults['tools_qrcode_back_color'])[:7])
  339. def on_export_frame(self, state):
  340. self.export_frame.setVisible(state)
  341. self.qrcode_button.setVisible(not state)
  342. def execute(self):
  343. text_data = self.text_data.get_value()
  344. if text_data == '':
  345. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
  346. return 'fail'
  347. # get the Gerber object on which the QRCode will be inserted
  348. selection_index = self.grb_object_combo.currentIndex()
  349. model_index = self.app.collection.index(selection_index, 0, self.grb_object_combo.rootModelIndex())
  350. try:
  351. self.grb_object = model_index.internalPointer().obj
  352. except Exception as e:
  353. log.debug("QRCode.execute() --> %s" % str(e))
  354. self.app.inform.emit('[WARNING_NOTCL] %s' % _("There is no Gerber object loaded ..."))
  355. return 'fail'
  356. # we can safely activate the mouse events
  357. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  358. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  359. self.kr = self.app.plotcanvas.graph_event_connect('key_release', self.on_key_release)
  360. self.proc = self.app.proc_container.new('%s...' % _("Generating QRCode geometry"))
  361. def job_thread_qr(app_obj):
  362. error_code = {
  363. 'L': qrcode.constants.ERROR_CORRECT_L,
  364. 'M': qrcode.constants.ERROR_CORRECT_M,
  365. 'Q': qrcode.constants.ERROR_CORRECT_Q,
  366. 'H': qrcode.constants.ERROR_CORRECT_H
  367. }[self.error_radio.get_value()]
  368. qr = qrcode.QRCode(
  369. version=self.version_entry.get_value(),
  370. error_correction=error_code,
  371. box_size=self.bsize_entry.get_value(),
  372. border=self.border_size_entry.get_value(),
  373. image_factory=qrcode.image.svg.SvgFragmentImage
  374. )
  375. qr.add_data(text_data)
  376. qr.make()
  377. svg_file = BytesIO()
  378. img = qr.make_image()
  379. img.save(svg_file)
  380. svg_text = StringIO(svg_file.getvalue().decode('UTF-8'))
  381. svg_geometry = self.convert_svg_to_geo(svg_text, units=self.units)
  382. self.qrcode_geometry = deepcopy(svg_geometry)
  383. svg_geometry = unary_union(svg_geometry).buffer(0.0000001).buffer(-0.0000001)
  384. self.qrcode_utility_geometry = svg_geometry
  385. # make a bounding box of the QRCode geometry to help drawing the utility geometry in case it is too
  386. # complicated
  387. try:
  388. a, b, c, d = self.qrcode_utility_geometry.bounds
  389. self.box_poly = box(minx=a, miny=b, maxx=c, maxy=d)
  390. except Exception as ee:
  391. log.debug("QRCode.make() bounds error --> %s" % str(ee))
  392. app_obj.call_source = 'qrcode_tool'
  393. app_obj.inform.emit(_("Click on the Destination point ..."))
  394. self.app.worker_task.emit({'fcn': job_thread_qr, 'params': [self.app]})
  395. def make(self, pos):
  396. self.on_exit()
  397. # make sure that the source object solid geometry is an Iterable
  398. if not isinstance(self.grb_object.solid_geometry, Iterable):
  399. self.grb_object.solid_geometry = [self.grb_object.solid_geometry]
  400. # I use the utility geometry (self.qrcode_utility_geometry) because it is already buffered
  401. geo_list = self.grb_object.solid_geometry
  402. if isinstance(self.grb_object.solid_geometry, MultiPolygon):
  403. geo_list = list(self.grb_object.solid_geometry.geoms)
  404. # this is the bounding box of the QRCode geometry
  405. a, b, c, d = self.qrcode_utility_geometry.bounds
  406. buff_val = self.border_size_entry.get_value() * (self.bsize_entry.get_value() / 10)
  407. if self.bb_radio.get_value() == 'r':
  408. mask_geo = box(a, b, c, d).buffer(buff_val)
  409. else:
  410. mask_geo = box(a, b, c, d).buffer(buff_val, join_style=2)
  411. # update the solid geometry with the cutout (if it is the case)
  412. new_solid_geometry = list()
  413. offset_mask_geo = translate(mask_geo, xoff=pos[0], yoff=pos[1])
  414. for poly in geo_list:
  415. if poly.contains(offset_mask_geo):
  416. new_solid_geometry.append(poly.difference(offset_mask_geo))
  417. else:
  418. if poly not in new_solid_geometry:
  419. new_solid_geometry.append(poly)
  420. geo_list = deepcopy(list(new_solid_geometry))
  421. # Polarity
  422. if self.pol_radio.get_value() == 'pos':
  423. working_geo = self.qrcode_utility_geometry
  424. else:
  425. working_geo = mask_geo.difference(self.qrcode_utility_geometry)
  426. try:
  427. for geo in working_geo:
  428. geo_list.append(translate(geo, xoff=pos[0], yoff=pos[1]))
  429. except TypeError:
  430. geo_list.append(translate(working_geo, xoff=pos[0], yoff=pos[1]))
  431. self.grb_object.solid_geometry = deepcopy(geo_list)
  432. box_size = float(self.bsize_entry.get_value()) / 10.0
  433. sort_apid = list()
  434. new_apid = '10'
  435. if self.grb_object.apertures:
  436. for k, v in list(self.grb_object.apertures.items()):
  437. sort_apid.append(int(k))
  438. sorted_apertures = sorted(sort_apid)
  439. max_apid = max(sorted_apertures)
  440. if max_apid >= 10:
  441. new_apid = str(max_apid + 1)
  442. else:
  443. new_apid = '10'
  444. # don't know if the condition is required since I already made sure above that the new_apid is a new one
  445. if new_apid not in self.grb_object.apertures:
  446. self.grb_object.apertures[new_apid] = dict()
  447. self.grb_object.apertures[new_apid]['geometry'] = list()
  448. self.grb_object.apertures[new_apid]['type'] = 'R'
  449. # TODO: HACK
  450. # I've artificially added 1% to the height and width because otherwise after loading the
  451. # exported file, it will not be correctly reconstructed (it will be made from multiple shapes instead of
  452. # one shape which show that the buffering didn't worked well). It may be the MM to INCH conversion.
  453. self.grb_object.apertures[new_apid]['height'] = deepcopy(box_size * 1.01)
  454. self.grb_object.apertures[new_apid]['width'] = deepcopy(box_size * 1.01)
  455. self.grb_object.apertures[new_apid]['size'] = deepcopy(math.sqrt(box_size ** 2 + box_size ** 2))
  456. if '0' not in self.grb_object.apertures:
  457. self.grb_object.apertures['0'] = dict()
  458. self.grb_object.apertures['0']['geometry'] = list()
  459. self.grb_object.apertures['0']['type'] = 'REG'
  460. self.grb_object.apertures['0']['size'] = 0.0
  461. # in case that the QRCode geometry is dropped onto a copper region (found in the '0' aperture)
  462. # make sure that I place a cutout there
  463. zero_elem = dict()
  464. zero_elem['clear'] = offset_mask_geo
  465. self.grb_object.apertures['0']['geometry'].append(deepcopy(zero_elem))
  466. try:
  467. a, b, c, d = self.grb_object.bounds()
  468. self.grb_object.options['xmin'] = a
  469. self.grb_object.options['ymin'] = b
  470. self.grb_object.options['xmax'] = c
  471. self.grb_object.options['ymax'] = d
  472. except Exception as e:
  473. log.debug("QRCode.make() bounds error --> %s" % str(e))
  474. try:
  475. for geo in self.qrcode_geometry:
  476. geo_elem = dict()
  477. geo_elem['solid'] = translate(geo, xoff=pos[0], yoff=pos[1])
  478. geo_elem['follow'] = translate(geo.centroid, xoff=pos[0], yoff=pos[1])
  479. self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
  480. except TypeError:
  481. geo_elem = dict()
  482. geo_elem['solid'] = self.qrcode_geometry
  483. self.grb_object.apertures[new_apid]['geometry'].append(deepcopy(geo_elem))
  484. # update the source file with the new geometry:
  485. self.grb_object.source_file = self.app.export_gerber(obj_name=self.grb_object.options['name'], filename=None,
  486. local_use=self.grb_object, use_thread=False)
  487. self.replot(obj=self.grb_object)
  488. self.app.inform.emit('[success] %s' % _("QRCode Tool done."))
  489. def draw_utility_geo(self, pos):
  490. # face = '#0000FF' + str(hex(int(0.2 * 255)))[2:]
  491. outline = '#0000FFAF'
  492. offset_geo = list()
  493. # I use the len of self.qrcode_geometry instead of the utility one because the complexity of the polygons is
  494. # better seen in this (bit what if the sel.qrcode_geometry is just one geo element? len will fail ...
  495. if len(self.qrcode_geometry) <= self.app.defaults["tools_qrcode_sel_limit"]:
  496. try:
  497. for poly in self.qrcode_utility_geometry:
  498. offset_geo.append(translate(poly.exterior, xoff=pos[0], yoff=pos[1]))
  499. for geo_int in poly.interiors:
  500. offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
  501. except TypeError:
  502. offset_geo.append(translate(self.qrcode_utility_geometry.exterior, xoff=pos[0], yoff=pos[1]))
  503. for geo_int in self.qrcode_utility_geometry.interiors:
  504. offset_geo.append(translate(geo_int, xoff=pos[0], yoff=pos[1]))
  505. else:
  506. offset_geo = [translate(self.box_poly, xoff=pos[0], yoff=pos[1])]
  507. for shape in offset_geo:
  508. self.shapes.add(shape, color=outline, update=True, layer=0, tolerance=None)
  509. if self.app.is_legacy is True:
  510. self.shapes.redraw()
  511. def delete_utility_geo(self):
  512. self.shapes.clear(update=True)
  513. self.shapes.redraw()
  514. def on_mouse_move(self, event):
  515. if self.app.is_legacy is False:
  516. event_pos = event.pos
  517. else:
  518. event_pos = (event.xdata, event.ydata)
  519. try:
  520. x = float(event_pos[0])
  521. y = float(event_pos[1])
  522. except TypeError:
  523. return
  524. pos_canvas = self.app.plotcanvas.translate_coords((x, y))
  525. # if GRID is active we need to get the snapped positions
  526. if self.app.grid_status() == True:
  527. pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  528. else:
  529. pos = pos_canvas
  530. dx = pos[0] - self.origin[0]
  531. dy = pos[1] - self.origin[1]
  532. # delete the utility geometry
  533. self.delete_utility_geo()
  534. self.draw_utility_geo((dx, dy))
  535. def on_mouse_release(self, event):
  536. # mouse click will be accepted only if the left button is clicked
  537. # this is necessary because right mouse click and middle mouse click
  538. # are used for panning on the canvas
  539. if self.app.is_legacy is False:
  540. event_pos = event.pos
  541. else:
  542. event_pos = (event.xdata, event.ydata)
  543. if event.button == 1:
  544. pos_canvas = self.app.plotcanvas.translate_coords(event_pos)
  545. self.delete_utility_geo()
  546. # if GRID is active we need to get the snapped positions
  547. if self.app.grid_status() == True:
  548. pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
  549. else:
  550. pos = pos_canvas
  551. dx = pos[0] - self.origin[0]
  552. dy = pos[1] - self.origin[1]
  553. self.make(pos=(dx, dy))
  554. def on_key_release(self, event):
  555. pass
  556. def convert_svg_to_geo(self, filename, object_type=None, flip=True, units='MM'):
  557. """
  558. Convert shapes from an SVG file into a geometry list.
  559. :param filename: A String Stream file.
  560. :param object_type: parameter passed further along. What kind the object will receive the SVG geometry
  561. :param flip: Flip the vertically.
  562. :type flip: bool
  563. :param units: FlatCAM units
  564. :return: None
  565. """
  566. # Parse into list of shapely objects
  567. svg_tree = ET.parse(filename)
  568. svg_root = svg_tree.getroot()
  569. # Change origin to bottom left
  570. # h = float(svg_root.get('height'))
  571. # w = float(svg_root.get('width'))
  572. h = svgparselength(svg_root.get('height'))[0] # TODO: No units support yet
  573. geos = getsvggeo(svg_root, object_type)
  574. if flip:
  575. geos = [translate(scale(g, 1.0, -1.0, origin=(0, 0)), yoff=h) for g in geos]
  576. # flatten the svg geometry for the case when the QRCode SVG is added into a Gerber object
  577. solid_geometry = list(self.flatten_list(geos))
  578. geos_text = getsvgtext(svg_root, object_type, units=units)
  579. if geos_text is not None:
  580. geos_text_f = []
  581. if flip:
  582. # Change origin to bottom left
  583. for i in geos_text:
  584. _, minimy, _, maximy = i.bounds
  585. h2 = (maximy - minimy) * 0.5
  586. geos_text_f.append(translate(scale(i, 1.0, -1.0, origin=(0, 0)), yoff=(h + h2)))
  587. if geos_text_f:
  588. solid_geometry += geos_text_f
  589. return solid_geometry
  590. def flatten_list(self, geo_list):
  591. for item in geo_list:
  592. if isinstance(item, Iterable) and not isinstance(item, (str, bytes)):
  593. yield from self.flatten_list(item)
  594. else:
  595. yield item
  596. def replot(self, obj):
  597. def worker_task():
  598. with self.app.proc_container.new('%s...' % _("Plotting")):
  599. obj.plot()
  600. self.app.worker_task.emit({'fcn': worker_task, 'params': []})
  601. def on_exit(self):
  602. if self.app.is_legacy is False:
  603. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  604. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  605. self.app.plotcanvas.graph_event_disconnect('key_release', self.on_key_release)
  606. else:
  607. self.app.plotcanvas.graph_event_disconnect(self.mm)
  608. self.app.plotcanvas.graph_event_disconnect(self.mr)
  609. self.app.plotcanvas.graph_event_disconnect(self.kr)
  610. # delete the utility geometry
  611. self.delete_utility_geo()
  612. self.app.call_source = 'app'
  613. def export_png_file(self):
  614. text_data = self.text_data.get_value()
  615. if text_data == '':
  616. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
  617. return 'fail'
  618. def job_thread_qr_png(app_obj, fname):
  619. error_code = {
  620. 'L': qrcode.constants.ERROR_CORRECT_L,
  621. 'M': qrcode.constants.ERROR_CORRECT_M,
  622. 'Q': qrcode.constants.ERROR_CORRECT_Q,
  623. 'H': qrcode.constants.ERROR_CORRECT_H
  624. }[self.error_radio.get_value()]
  625. qr = qrcode.QRCode(
  626. version=self.version_entry.get_value(),
  627. error_correction=error_code,
  628. box_size=self.bsize_entry.get_value(),
  629. border=self.border_size_entry.get_value(),
  630. image_factory=qrcode.image.pil.PilImage
  631. )
  632. qr.add_data(text_data)
  633. qr.make(fit=True)
  634. img = qr.make_image(fill_color=self.fill_color_entry.get_value(),
  635. back_color=self.back_color_entry.get_value())
  636. img.save(fname)
  637. app_obj.call_source = 'qrcode_tool'
  638. name = 'qr_code'
  639. _filter = "PNG File (*.png);;All Files (*.*)"
  640. try:
  641. filename, _f = QtWidgets.QFileDialog.getSaveFileName(
  642. caption=_("Export PNG"),
  643. directory=self.app.get_last_save_folder() + '/' + str(name) + '_png',
  644. filter=_filter)
  645. except TypeError:
  646. filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export PNG"), filter=_filter)
  647. filename = str(filename)
  648. if filename == "":
  649. self.app.inform.emit('[WARNING_NOTCL]%s' % _(" Export PNG cancelled."))
  650. return
  651. else:
  652. self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]})
  653. def export_svg_file(self):
  654. text_data = self.text_data.get_value()
  655. if text_data == '':
  656. self.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled. There is no QRCode Data in the text box."))
  657. return 'fail'
  658. def job_thread_qr_svg(app_obj, fname):
  659. error_code = {
  660. 'L': qrcode.constants.ERROR_CORRECT_L,
  661. 'M': qrcode.constants.ERROR_CORRECT_M,
  662. 'Q': qrcode.constants.ERROR_CORRECT_Q,
  663. 'H': qrcode.constants.ERROR_CORRECT_H
  664. }[self.error_radio.get_value()]
  665. qr = qrcode.QRCode(
  666. version=self.version_entry.get_value(),
  667. error_correction=error_code,
  668. box_size=self.bsize_entry.get_value(),
  669. border=self.border_size_entry.get_value(),
  670. image_factory=qrcode.image.svg.SvgPathImage
  671. )
  672. qr.add_data(text_data)
  673. img = qr.make_image(fill_color=self.fill_color_entry.get_value(),
  674. back_color=self.back_color_entry.get_value())
  675. img.save(fname)
  676. app_obj.call_source = 'qrcode_tool'
  677. name = 'qr_code'
  678. _filter = "SVG File (*.svg);;All Files (*.*)"
  679. try:
  680. filename, _f = QtWidgets.QFileDialog.getSaveFileName(
  681. caption=_("Export SVG"),
  682. directory=self.app.get_last_save_folder() + '/' + str(name) + '_svg',
  683. filter=_filter)
  684. except TypeError:
  685. filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG"), filter=_filter)
  686. filename = str(filename)
  687. if filename == "":
  688. self.app.inform.emit('[WARNING_NOTCL]%s' % _(" Export SVG cancelled."))
  689. return
  690. else:
  691. self.app.worker_task.emit({'fcn': job_thread_qr_svg, 'params': [self.app, filename]})
  692. def on_qrcode_fill_color_entry(self):
  693. color = self.fill_color_entry.get_value()
  694. self.fill_color_button.setStyleSheet("background-color:%s" % str(color))
  695. def on_qrcode_fill_color_button(self):
  696. current_color = QtGui.QColor(self.fill_color_entry.get_value())
  697. c_dialog = QtWidgets.QColorDialog()
  698. fill_color = c_dialog.getColor(initial=current_color)
  699. if fill_color.isValid() is False:
  700. return
  701. self.fill_color_button.setStyleSheet("background-color:%s" % str(fill_color.name()))
  702. new_val_sel = str(fill_color.name())
  703. self.fill_color_entry.set_value(new_val_sel)
  704. def on_qrcode_back_color_entry(self):
  705. color = self.back_color_entry.get_value()
  706. self.back_color_button.setStyleSheet("background-color:%s" % str(color))
  707. def on_qrcode_back_color_button(self):
  708. current_color = QtGui.QColor(self.back_color_entry.get_value())
  709. c_dialog = QtWidgets.QColorDialog()
  710. back_color = c_dialog.getColor(initial=current_color)
  711. if back_color.isValid() is False:
  712. return
  713. self.back_color_button.setStyleSheet("background-color:%s" % str(back_color.name()))
  714. new_val_sel = str(back_color.name())
  715. self.back_color_entry.set_value(new_val_sel)
  716. def on_transparent_back_color(self, state):
  717. if state:
  718. self.back_color_entry.setDisabled(True)
  719. self.back_color_button.setDisabled(True)
  720. self.old_back_color = self.back_color_entry.get_value()
  721. self.back_color_entry.set_value('transparent')
  722. else:
  723. self.back_color_entry.setDisabled(False)
  724. self.back_color_button.setDisabled(False)
  725. self.back_color_entry.set_value(self.old_back_color)