ToolFilm.py 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 3/10/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtGui, QtCore, QtWidgets
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, \
  10. OptionalHideInputSection, OptionalInputSection, FCComboBox
  11. from copy import deepcopy
  12. import logging
  13. from shapely.geometry import Polygon, MultiPolygon, Point
  14. from reportlab.graphics import renderPDF
  15. from reportlab.pdfgen import canvas
  16. from reportlab.graphics import renderPM
  17. from reportlab.lib.units import inch, mm
  18. from reportlab.lib.pagesizes import landscape, portrait
  19. from svglib.svglib import svg2rlg
  20. from xml.dom.minidom import parseString as parse_xml_string
  21. from lxml import etree as ET
  22. from io import StringIO
  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 Film(FlatCAMTool):
  31. toolName = _("Film PCB")
  32. def __init__(self, app):
  33. FlatCAMTool.__init__(self, app)
  34. self.decimals = self.app.decimals
  35. # Title
  36. title_label = QtWidgets.QLabel("%s" % self.toolName)
  37. title_label.setStyleSheet("""
  38. QLabel
  39. {
  40. font-size: 16px;
  41. font-weight: bold;
  42. }
  43. """)
  44. self.layout.addWidget(title_label)
  45. # Form Layout
  46. grid0 = QtWidgets.QGridLayout()
  47. self.layout.addLayout(grid0)
  48. grid0.setColumnStretch(0, 0)
  49. grid0.setColumnStretch(1, 1)
  50. # Type of object for which to create the film
  51. self.tf_type_obj_combo = QtWidgets.QComboBox()
  52. self.tf_type_obj_combo.addItem("Gerber")
  53. self.tf_type_obj_combo.addItem("Excellon")
  54. self.tf_type_obj_combo.addItem("Geometry")
  55. # we get rid of item1 ("Excellon") as it is not suitable for creating film
  56. self.tf_type_obj_combo.view().setRowHidden(1, True)
  57. self.tf_type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  58. self.tf_type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
  59. self.tf_type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Object Type"))
  60. self.tf_type_obj_combo_label.setToolTip(
  61. _("Specify the type of object for which to create the film.\n"
  62. "The object can be of type: Gerber or Geometry.\n"
  63. "The selection here decide the type of objects that will be\n"
  64. "in the Film Object combobox.")
  65. )
  66. grid0.addWidget(self.tf_type_obj_combo_label, 0, 0)
  67. grid0.addWidget(self.tf_type_obj_combo, 0, 1)
  68. # List of objects for which we can create the film
  69. self.tf_object_combo = QtWidgets.QComboBox()
  70. self.tf_object_combo.setModel(self.app.collection)
  71. self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  72. self.tf_object_combo.setCurrentIndex(1)
  73. self.tf_object_label = QtWidgets.QLabel('%s:' % _("Film Object"))
  74. self.tf_object_label.setToolTip(
  75. _("Object for which to create the film.")
  76. )
  77. grid0.addWidget(self.tf_object_label, 1, 0)
  78. grid0.addWidget(self.tf_object_combo, 1, 1)
  79. # Type of Box Object to be used as an envelope for film creation
  80. # Within this we can create negative
  81. self.tf_type_box_combo = QtWidgets.QComboBox()
  82. self.tf_type_box_combo.addItem("Gerber")
  83. self.tf_type_box_combo.addItem("Excellon")
  84. self.tf_type_box_combo.addItem("Geometry")
  85. # we get rid of item1 ("Excellon") as it is not suitable for box when creating film
  86. self.tf_type_box_combo.view().setRowHidden(1, True)
  87. self.tf_type_box_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
  88. self.tf_type_box_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
  89. self.tf_type_box_combo_label = QtWidgets.QLabel(_("Box Type:"))
  90. self.tf_type_box_combo_label.setToolTip(
  91. _("Specify the type of object to be used as an container for\n"
  92. "film creation. It can be: Gerber or Geometry type."
  93. "The selection here decide the type of objects that will be\n"
  94. "in the Box Object combobox.")
  95. )
  96. grid0.addWidget(self.tf_type_box_combo_label, 2, 0)
  97. grid0.addWidget(self.tf_type_box_combo, 2, 1)
  98. # Box
  99. self.tf_box_combo = QtWidgets.QComboBox()
  100. self.tf_box_combo.setModel(self.app.collection)
  101. self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  102. self.tf_box_combo.setCurrentIndex(1)
  103. self.tf_box_combo_label = QtWidgets.QLabel('%s:' % _("Box Object"))
  104. self.tf_box_combo_label.setToolTip(
  105. _("The actual object that is used a container for the\n "
  106. "selected object for which we create the film.\n"
  107. "Usually it is the PCB outline but it can be also the\n"
  108. "same object for which the film is created.")
  109. )
  110. grid0.addWidget(self.tf_box_combo_label, 3, 0)
  111. grid0.addWidget(self.tf_box_combo, 3, 1)
  112. grid0.addWidget(QtWidgets.QLabel(''), 4, 0)
  113. self.film_adj_label = QtWidgets.QLabel('<b>%s</b>' % _("Film Adjustments"))
  114. self.film_adj_label.setToolTip(
  115. _("Sometime the printers will distort the print shape, especially the Laser types.\n"
  116. "This section provide the tools to compensate for the print distortions.")
  117. )
  118. grid0.addWidget(self.film_adj_label, 5, 0, 1, 2)
  119. # Scale Geometry
  120. self.film_scale_cb = FCCheckBox('%s' % _("Scale Film geometry"))
  121. self.film_scale_cb.setToolTip(
  122. _("A value greater than 1 will stretch the film\n"
  123. "while a value less than 1 will jolt it.")
  124. )
  125. self.film_scale_cb.setStyleSheet(
  126. """
  127. QCheckBox {font-weight: bold; color: black}
  128. """
  129. )
  130. grid0.addWidget(self.film_scale_cb, 6, 0, 1, 2)
  131. self.film_scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
  132. self.film_scalex_entry = FCDoubleSpinner()
  133. self.film_scalex_entry.set_range(-999.9999, 999.9999)
  134. self.film_scalex_entry.set_precision(self.decimals)
  135. self.film_scalex_entry.setSingleStep(0.01)
  136. grid0.addWidget(self.film_scalex_label, 7, 0)
  137. grid0.addWidget(self.film_scalex_entry, 7, 1)
  138. self.film_scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
  139. self.film_scaley_entry = FCDoubleSpinner()
  140. self.film_scaley_entry.set_range(-999.9999, 999.9999)
  141. self.film_scaley_entry.set_precision(self.decimals)
  142. self.film_scaley_entry.setSingleStep(0.01)
  143. grid0.addWidget(self.film_scaley_label, 8, 0)
  144. grid0.addWidget(self.film_scaley_entry, 8, 1)
  145. self.ois_scale = OptionalInputSection(self.film_scale_cb, [self.film_scalex_label, self.film_scalex_entry,
  146. self.film_scaley_label, self.film_scaley_entry])
  147. separator_line = QtWidgets.QFrame()
  148. separator_line.setFrameShape(QtWidgets.QFrame.HLine)
  149. separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
  150. grid0.addWidget(separator_line, 9, 0, 1, 2)
  151. # Skew Geometry
  152. self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
  153. self.film_skew_cb.setToolTip(
  154. _("Positive values will skew to the right\n"
  155. "while negative values will skew to the left.")
  156. )
  157. self.film_skew_cb.setStyleSheet(
  158. """
  159. QCheckBox {font-weight: bold; color: black}
  160. """
  161. )
  162. grid0.addWidget(self.film_skew_cb, 10, 0, 1, 2)
  163. self.film_skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
  164. self.film_skewx_entry = FCDoubleSpinner()
  165. self.film_skewx_entry.set_range(-999.9999, 999.9999)
  166. self.film_skewx_entry.set_precision(self.decimals)
  167. self.film_skewx_entry.setSingleStep(0.01)
  168. grid0.addWidget(self.film_skewx_label, 11, 0)
  169. grid0.addWidget(self.film_skewx_entry, 11, 1)
  170. self.film_skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
  171. self.film_skewy_entry = FCDoubleSpinner()
  172. self.film_skewy_entry.set_range(-999.9999, 999.9999)
  173. self.film_skewy_entry.set_precision(self.decimals)
  174. self.film_skewy_entry.setSingleStep(0.01)
  175. grid0.addWidget(self.film_skewy_label, 12, 0)
  176. grid0.addWidget(self.film_skewy_entry, 12, 1)
  177. self.film_skew_ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
  178. self.film_skew_ref_label.setToolTip(
  179. _("The reference point to be used as origin for the skew.\n"
  180. "It can be one of the four points of the geometry bounding box.")
  181. )
  182. self.film_skew_reference = RadioSet([{'label': _('Bottom Left'), 'value': 'bottomleft'},
  183. {'label': _('Top Left'), 'value': 'topleft'},
  184. {'label': _('Bottom Right'), 'value': 'bottomright'},
  185. {'label': _('Top right'), 'value': 'topright'}],
  186. orientation='vertical',
  187. stretch=False)
  188. grid0.addWidget(self.film_skew_ref_label, 13, 0)
  189. grid0.addWidget(self.film_skew_reference, 13, 1)
  190. self.ois_skew = OptionalInputSection(self.film_skew_cb, [self.film_skewx_label, self.film_skewx_entry,
  191. self.film_skewy_label, self.film_skewy_entry,
  192. self.film_skew_reference])
  193. separator_line1 = QtWidgets.QFrame()
  194. separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
  195. separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
  196. grid0.addWidget(separator_line1, 14, 0, 1, 2)
  197. # Mirror Geometry
  198. self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
  199. self.film_mirror_cb.setToolTip(
  200. _("Mirror the film geometry on the selected axis or on both.")
  201. )
  202. self.film_mirror_cb.setStyleSheet(
  203. """
  204. QCheckBox {font-weight: bold; color: black}
  205. """
  206. )
  207. grid0.addWidget(self.film_mirror_cb, 15, 0, 1, 2)
  208. self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
  209. {'label': _('X'), 'value': 'x'},
  210. {'label': _('Y'), 'value': 'y'},
  211. {'label': _('Both'), 'value': 'both'}],
  212. stretch=False)
  213. self.film_mirror_axis_label = QtWidgets.QLabel('%s:' % _("Mirror axis"))
  214. grid0.addWidget(self.film_mirror_axis_label, 16, 0)
  215. grid0.addWidget(self.film_mirror_axis, 16, 1)
  216. self.ois_mirror = OptionalInputSection(self.film_mirror_cb,
  217. [self.film_mirror_axis_label, self.film_mirror_axis])
  218. separator_line2 = QtWidgets.QFrame()
  219. separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
  220. separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
  221. grid0.addWidget(separator_line2, 17, 0, 1, 2)
  222. self.film_param_label = QtWidgets.QLabel('<b>%s</b>' % _("Film Parameters"))
  223. grid0.addWidget(self.film_param_label, 18, 0, 1, 2)
  224. # Scale Stroke size
  225. self.film_scale_stroke_entry = FCDoubleSpinner()
  226. self.film_scale_stroke_entry.set_range(-999.9999, 999.9999)
  227. self.film_scale_stroke_entry.setSingleStep(0.01)
  228. self.film_scale_stroke_entry.set_precision(self.decimals)
  229. self.film_scale_stroke_label = QtWidgets.QLabel('%s:' % _("Scale Stroke"))
  230. self.film_scale_stroke_label.setToolTip(
  231. _("Scale the line stroke thickness of each feature in the SVG file.\n"
  232. "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
  233. "therefore the fine features may be more affected by this parameter.")
  234. )
  235. grid0.addWidget(self.film_scale_stroke_label, 19, 0)
  236. grid0.addWidget(self.film_scale_stroke_entry, 19, 1)
  237. grid0.addWidget(QtWidgets.QLabel(''), 20, 0)
  238. # Film Type
  239. self.film_type = RadioSet([{'label': _('Positive'), 'value': 'pos'},
  240. {'label': _('Negative'), 'value': 'neg'}],
  241. stretch=False)
  242. self.film_type_label = QtWidgets.QLabel(_("Film Type:"))
  243. self.film_type_label.setToolTip(
  244. _("Generate a Positive black film or a Negative film.\n"
  245. "Positive means that it will print the features\n"
  246. "with black on a white canvas.\n"
  247. "Negative means that it will print the features\n"
  248. "with white on a black canvas.\n"
  249. "The Film format is SVG.")
  250. )
  251. grid0.addWidget(self.film_type_label, 21, 0)
  252. grid0.addWidget(self.film_type, 21, 1)
  253. # Boundary for negative film generation
  254. self.boundary_entry = FCDoubleSpinner()
  255. self.boundary_entry.set_range(-999.9999, 999.9999)
  256. self.boundary_entry.setSingleStep(0.01)
  257. self.boundary_entry.set_precision(self.decimals)
  258. self.boundary_label = QtWidgets.QLabel('%s:' % _("Border"))
  259. self.boundary_label.setToolTip(
  260. _("Specify a border around the object.\n"
  261. "Only for negative film.\n"
  262. "It helps if we use as a Box Object the same \n"
  263. "object as in Film Object. It will create a thick\n"
  264. "black bar around the actual print allowing for a\n"
  265. "better delimitation of the outline features which are of\n"
  266. "white color like the rest and which may confound with the\n"
  267. "surroundings if not for this border.")
  268. )
  269. grid0.addWidget(self.boundary_label, 22, 0)
  270. grid0.addWidget(self.boundary_entry, 22, 1)
  271. self.boundary_label.hide()
  272. self.boundary_entry.hide()
  273. # Punch Drill holes
  274. self.punch_cb = FCCheckBox(_("Punch drill holes"))
  275. self.punch_cb.setToolTip(_("When checked the generated film will have holes in pads when\n"
  276. "the generated film is positive. This is done to help drilling,\n"
  277. "when done manually."))
  278. grid0.addWidget(self.punch_cb, 23, 0, 1, 2)
  279. # this way I can hide/show the frame
  280. self.punch_frame = QtWidgets.QFrame()
  281. self.punch_frame.setContentsMargins(0, 0, 0, 0)
  282. self.layout.addWidget(self.punch_frame)
  283. punch_grid = QtWidgets.QGridLayout()
  284. punch_grid.setContentsMargins(0, 0, 0, 0)
  285. self.punch_frame.setLayout(punch_grid)
  286. punch_grid.setColumnStretch(0, 0)
  287. punch_grid.setColumnStretch(1, 1)
  288. self.ois_p = OptionalHideInputSection(self.punch_cb, [self.punch_frame])
  289. self.source_label = QtWidgets.QLabel('%s:' % _("Source"))
  290. self.source_label.setToolTip(
  291. _("The punch hole source can be:\n"
  292. "- Excellon -> an Excellon holes center will serve as reference.\n"
  293. "- Pad Center -> will try to use the pads center as reference.")
  294. )
  295. self.source_punch = RadioSet([{'label': _('Excellon'), 'value': 'exc'},
  296. {'label': _('Pad center'), 'value': 'pad'}],
  297. stretch=False)
  298. punch_grid.addWidget(self.source_label, 0, 0)
  299. punch_grid.addWidget(self.source_punch, 0, 1)
  300. self.exc_label = QtWidgets.QLabel('%s:' % _("Excellon Obj"))
  301. self.exc_label.setToolTip(
  302. _("Remove the geometry of Excellon from the Film to create the holes in pads.")
  303. )
  304. self.exc_combo = QtWidgets.QComboBox()
  305. self.exc_combo.setModel(self.app.collection)
  306. self.exc_combo.setRootModelIndex(self.app.collection.index(1, 0, QtCore.QModelIndex()))
  307. self.exc_combo.setCurrentIndex(1)
  308. punch_grid.addWidget(self.exc_label, 1, 0)
  309. punch_grid.addWidget(self.exc_combo, 1, 1)
  310. self.exc_label.hide()
  311. self.exc_combo.hide()
  312. self.punch_size_label = QtWidgets.QLabel('%s:' % _("Punch Size"))
  313. self.punch_size_label.setToolTip(_("The value here will control how big is the punch hole in the pads."))
  314. self.punch_size_spinner = FCDoubleSpinner()
  315. self.punch_size_spinner.set_range(0, 999.9999)
  316. self.punch_size_spinner.setSingleStep(0.1)
  317. self.punch_size_spinner.set_precision(self.decimals)
  318. punch_grid.addWidget(self.punch_size_label, 2, 0)
  319. punch_grid.addWidget(self.punch_size_spinner, 2, 1)
  320. self.punch_size_label.hide()
  321. self.punch_size_spinner.hide()
  322. grid1 = QtWidgets.QGridLayout()
  323. self.layout.addLayout(grid1)
  324. grid1.setColumnStretch(0, 0)
  325. grid1.setColumnStretch(1, 1)
  326. separator_line3 = QtWidgets.QFrame()
  327. separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
  328. separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
  329. grid1.addWidget(separator_line3, 0, 0, 1, 2)
  330. # File type
  331. self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'},
  332. {'label': _('PNG'), 'value': 'png'},
  333. {'label': _('PDF'), 'value': 'pdf'}
  334. ], stretch=False)
  335. self.file_type_label = QtWidgets.QLabel(_("Film Type:"))
  336. self.file_type_label.setToolTip(
  337. _("The file type of the saved film. Can be:\n"
  338. "- 'SVG' -> open-source vectorial format\n"
  339. "- 'PNG' -> raster image\n"
  340. "- 'PDF' -> portable document format")
  341. )
  342. grid1.addWidget(self.file_type_label, 1, 0)
  343. grid1.addWidget(self.file_type_radio, 1, 1)
  344. # Page orientation
  345. self.orientation_label = QtWidgets.QLabel('%s:' % _("Page Orientation"))
  346. self.orientation_label.setToolTip(_("Can be:\n"
  347. "- Portrait\n"
  348. "- Landscape"))
  349. self.orientation_radio = RadioSet([{'label': _('Portrait'), 'value': 'p'},
  350. {'label': _('Landscape'), 'value': 'l'},
  351. ], stretch=False)
  352. grid1.addWidget(self.orientation_label, 2, 0)
  353. grid1.addWidget(self.orientation_radio, 2, 1)
  354. # Page Size
  355. self.pagesize_label = QtWidgets.QLabel('%s:' % _("Page Size"))
  356. self.pagesize_label.setToolTip(_("A selection of standard ISO 216 page sizes."))
  357. self.pagesize_combo = FCComboBox()
  358. self.pagesize = dict()
  359. self.pagesize.update(
  360. {
  361. 'Bounds': None,
  362. 'A0': (841*mm, 1189*mm),
  363. 'A1': (594*mm, 841*mm),
  364. 'A2': (420*mm, 594*mm),
  365. 'A3': (297*mm, 420*mm),
  366. 'A4': (210*mm, 297*mm),
  367. 'A5': (148*mm, 210*mm),
  368. 'A6': (105*mm, 148*mm),
  369. 'A7': (74*mm, 105*mm),
  370. 'A8': (52*mm, 74*mm),
  371. 'A9': (37*mm, 52*mm),
  372. 'A10': (26*mm, 37*mm),
  373. 'B0': (1000*mm, 1414*mm),
  374. 'B1': (707*mm, 1000*mm),
  375. 'B2': (500*mm, 707*mm),
  376. 'B3': (353*mm, 500*mm),
  377. 'B4': (250*mm, 353*mm),
  378. 'B5': (176*mm, 250*mm),
  379. 'B6': (125*mm, 176*mm),
  380. 'B7': (88*mm, 125*mm),
  381. 'B8': (62*mm, 88*mm),
  382. 'B9': (44*mm, 62*mm),
  383. 'B10': (31*mm, 44*mm),
  384. 'C0': (917*mm, 1297*mm),
  385. 'C1': (648*mm, 917*mm),
  386. 'C2': (458*mm, 648*mm),
  387. 'C3': (324*mm, 458*mm),
  388. 'C4': (229*mm, 324*mm),
  389. 'C5': (162*mm, 229*mm),
  390. 'C6': (114*mm, 162*mm),
  391. 'C7': (81*mm, 114*mm),
  392. 'C8': (57*mm, 81*mm),
  393. 'C9': (40*mm, 57*mm),
  394. 'C10': (28*mm, 40*mm),
  395. # American paper sizes
  396. 'LETTER': (8.5*inch, 11*inch),
  397. 'LEGAL': (8.5*inch, 14*inch),
  398. 'ELEVENSEVENTEEN': (11*inch, 17*inch),
  399. # From https://en.wikipedia.org/wiki/Paper_size
  400. 'JUNIOR_LEGAL': (5*inch, 8*inch),
  401. 'HALF_LETTER': (5.5*inch, 8*inch),
  402. 'GOV_LETTER': (8*inch, 10.5*inch),
  403. 'GOV_LEGAL': (8.5*inch, 13*inch),
  404. 'LEDGER': (17*inch, 11*inch),
  405. }
  406. )
  407. page_size_list = list(self.pagesize.keys())
  408. self.pagesize_combo.addItems(page_size_list)
  409. grid1.addWidget(self.pagesize_label, 3, 0)
  410. grid1.addWidget(self.pagesize_combo, 3, 1)
  411. self.on_film_type(val='hide')
  412. # Buttons
  413. self.film_object_button = QtWidgets.QPushButton(_("Save Film"))
  414. self.film_object_button.setToolTip(
  415. _("Create a Film for the selected object, within\n"
  416. "the specified box. Does not create a new \n "
  417. "FlatCAM object, but directly save it in the\n"
  418. "selected format.")
  419. )
  420. self.film_object_button.setStyleSheet("""
  421. QPushButton
  422. {
  423. font-weight: bold;
  424. }
  425. """)
  426. grid1.addWidget(self.film_object_button, 4, 0, 1, 2)
  427. self.layout.addStretch()
  428. # ## Reset Tool
  429. self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
  430. self.reset_button.setToolTip(
  431. _("Will reset the tool parameters.")
  432. )
  433. self.reset_button.setStyleSheet("""
  434. QPushButton
  435. {
  436. font-weight: bold;
  437. }
  438. """)
  439. self.layout.addWidget(self.reset_button)
  440. self.units = self.app.defaults['units']
  441. # ## Signals
  442. self.film_object_button.clicked.connect(self.on_film_creation)
  443. self.tf_type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  444. self.tf_type_box_combo.currentIndexChanged.connect(self.on_type_box_index_changed)
  445. self.film_type.activated_custom.connect(self.on_film_type)
  446. self.source_punch.activated_custom.connect(self.on_punch_source)
  447. self.file_type_radio.activated_custom.connect(self.on_file_type)
  448. self.reset_button.clicked.connect(self.set_tool_ui)
  449. def on_type_obj_index_changed(self, index):
  450. obj_type = self.tf_type_obj_combo.currentIndex()
  451. self.tf_object_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  452. self.tf_object_combo.setCurrentIndex(0)
  453. def on_type_box_index_changed(self, index):
  454. obj_type = self.tf_type_box_combo.currentIndex()
  455. self.tf_box_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  456. self.tf_box_combo.setCurrentIndex(0)
  457. def run(self, toggle=True):
  458. self.app.report_usage("ToolFilm()")
  459. if toggle:
  460. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  461. if self.app.ui.splitter.sizes()[0] == 0:
  462. self.app.ui.splitter.setSizes([1, 1])
  463. else:
  464. try:
  465. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  466. # if tab is populated with the tool but it does not have the focus, focus on it
  467. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  468. # focus on Tool Tab
  469. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  470. else:
  471. self.app.ui.splitter.setSizes([0, 1])
  472. except AttributeError:
  473. pass
  474. else:
  475. if self.app.ui.splitter.sizes()[0] == 0:
  476. self.app.ui.splitter.setSizes([1, 1])
  477. FlatCAMTool.run(self)
  478. self.set_tool_ui()
  479. self.app.ui.notebook.setTabText(2, _("Film Tool"))
  480. def install(self, icon=None, separator=None, **kwargs):
  481. FlatCAMTool.install(self, icon, separator, shortcut='ALT+L', **kwargs)
  482. def set_tool_ui(self):
  483. self.reset_fields()
  484. f_type = self.app.defaults["tools_film_type"] if self.app.defaults["tools_film_type"] else 'neg'
  485. self.film_type.set_value(str(f_type))
  486. self.on_film_type(val=f_type)
  487. b_entry = self.app.defaults["tools_film_boundary"] if self.app.defaults["tools_film_boundary"] else 0.0
  488. self.boundary_entry.set_value(float(b_entry))
  489. scale_stroke_width = self.app.defaults["tools_film_scale_stroke"] if \
  490. self.app.defaults["tools_film_scale_stroke"] else 0.0
  491. self.film_scale_stroke_entry.set_value(int(scale_stroke_width))
  492. self.punch_cb.set_value(False)
  493. self.source_punch.set_value('exc')
  494. self.film_scale_cb.set_value(self.app.defaults["tools_film_scale_cb"])
  495. self.film_scalex_entry.set_value(float(self.app.defaults["tools_film_scale_x_entry"]))
  496. self.film_scaley_entry.set_value(float(self.app.defaults["tools_film_scale_y_entry"]))
  497. self.film_skew_cb.set_value(self.app.defaults["tools_film_skew_cb"])
  498. self.film_skewx_entry.set_value(float(self.app.defaults["tools_film_skew_x_entry"]))
  499. self.film_skewy_entry.set_value(float(self.app.defaults["tools_film_skew_y_entry"]))
  500. self.film_skew_reference.set_value(self.app.defaults["tools_film_skew_ref_radio"])
  501. self.film_mirror_cb.set_value(self.app.defaults["tools_film_mirror_cb"])
  502. self.film_mirror_axis.set_value(self.app.defaults["tools_film_mirror_axis_radio"])
  503. self.file_type_radio.set_value(self.app.defaults["tools_film_file_type_radio"])
  504. self.orientation_radio.set_value(self.app.defaults["tools_film_orientation"])
  505. self.pagesize_combo.set_value(self.app.defaults["tools_film_pagesize"])
  506. def on_film_type(self, val):
  507. type_of_film = val
  508. if type_of_film == 'neg':
  509. self.boundary_label.show()
  510. self.boundary_entry.show()
  511. self.punch_cb.set_value(False) # required so the self.punch_frame it's hidden also by the signal emitted
  512. self.punch_cb.hide()
  513. else:
  514. self.boundary_label.hide()
  515. self.boundary_entry.hide()
  516. self.punch_cb.show()
  517. def on_file_type(self, val):
  518. if val == 'pdf':
  519. self.orientation_label.show()
  520. self.orientation_radio.show()
  521. self.pagesize_label.show()
  522. self.pagesize_combo.show()
  523. else:
  524. self.orientation_label.hide()
  525. self.orientation_radio.hide()
  526. self.pagesize_label.hide()
  527. self.pagesize_combo.hide()
  528. def on_punch_source(self, val):
  529. if val == 'pad' and self.punch_cb.get_value():
  530. self.punch_size_label.show()
  531. self.punch_size_spinner.show()
  532. self.exc_label.hide()
  533. self.exc_combo.hide()
  534. else:
  535. self.punch_size_label.hide()
  536. self.punch_size_spinner.hide()
  537. self.exc_label.show()
  538. self.exc_combo.show()
  539. if val == 'pad' and self.tf_type_obj_combo.currentText() == 'Geometry':
  540. self.source_punch.set_value('exc')
  541. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Using the Pad center does not work on Geometry objects. "
  542. "Only a Gerber object has pads."))
  543. def on_film_creation(self):
  544. log.debug("ToolFilm.Film.on_film_creation() started ...")
  545. try:
  546. name = self.tf_object_combo.currentText()
  547. except Exception:
  548. self.app.inform.emit('[ERROR_NOTCL] %s' %
  549. _("No FlatCAM object selected. Load an object for Film and retry."))
  550. return
  551. try:
  552. boxname = self.tf_box_combo.currentText()
  553. except Exception:
  554. self.app.inform.emit('[ERROR_NOTCL] %s' %
  555. _("No FlatCAM object selected. Load an object for Box and retry."))
  556. return
  557. if name == '' or boxname == '':
  558. self.app.inform.emit('[ERROR_NOTCL] %s' % _("No FlatCAM object selected."))
  559. return
  560. scale_stroke_width = float(self.film_scale_stroke_entry.get_value())
  561. source = self.source_punch.get_value()
  562. file_type = self.file_type_radio.get_value()
  563. # #################################################################
  564. # ################ STARTING THE JOB ###############################
  565. # #################################################################
  566. self.app.inform.emit(_("Generating Film ..."))
  567. if self.film_type.get_value() == "pos":
  568. if self.punch_cb.get_value() is False:
  569. self.generate_positive_normal_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
  570. else:
  571. self.generate_positive_punched_film(name, boxname, source, factor=scale_stroke_width, ftype=file_type)
  572. else:
  573. self.generate_negative_film(name, boxname, factor=scale_stroke_width, ftype=file_type)
  574. def generate_positive_normal_film(self, name, boxname, factor, ftype='svg'):
  575. log.debug("ToolFilm.Film.generate_positive_normal_film() started ...")
  576. scale_factor_x = None
  577. scale_factor_y = None
  578. skew_factor_x = None
  579. skew_factor_y = None
  580. mirror = None
  581. skew_reference = 'center'
  582. if self.film_scale_cb.get_value():
  583. if self.film_scalex_entry.get_value() != 1.0:
  584. scale_factor_x = self.film_scalex_entry.get_value()
  585. if self.film_scaley_entry.get_value() != 1.0:
  586. scale_factor_y = self.film_scaley_entry.get_value()
  587. if self.film_skew_cb.get_value():
  588. if self.film_skewx_entry.get_value() != 0.0:
  589. skew_factor_x = self.film_skewx_entry.get_value()
  590. if self.film_skewy_entry.get_value() != 0.0:
  591. skew_factor_y = self.film_skewy_entry.get_value()
  592. skew_reference = self.film_skew_reference.get_value()
  593. if self.film_mirror_cb.get_value():
  594. if self.film_mirror_axis.get_value() != 'none':
  595. mirror = self.film_mirror_axis.get_value()
  596. if ftype == 'svg':
  597. filter_ext = "SVG Files (*.SVG);;"\
  598. "All Files (*.*)"
  599. elif ftype == 'png':
  600. filter_ext = "PNG Files (*.PNG);;" \
  601. "All Files (*.*)"
  602. else:
  603. filter_ext = "PDF Files (*.PDF);;" \
  604. "All Files (*.*)"
  605. try:
  606. filename, _f = QtWidgets.QFileDialog.getSaveFileName(
  607. caption=_("Export positive film"),
  608. directory=self.app.get_last_save_folder() + '/' + name + '_film',
  609. filter=filter_ext)
  610. except TypeError:
  611. filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export positive film"))
  612. filename = str(filename)
  613. if str(filename) == "":
  614. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export positive film cancelled."))
  615. return
  616. else:
  617. pagesize = self.pagesize_combo.get_value()
  618. orientation = self.orientation_radio.get_value()
  619. color = self.app.defaults['tools_film_color']
  620. self.export_positive(name, boxname, filename,
  621. scale_stroke_factor=factor,
  622. scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
  623. skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
  624. skew_reference=skew_reference,
  625. mirror=mirror,
  626. pagesize=pagesize, orientation=orientation, color=color, opacity=1.0,
  627. ftype=ftype
  628. )
  629. def generate_positive_punched_film(self, name, boxname, source, factor, ftype='svg'):
  630. film_obj = self.app.collection.get_by_name(name)
  631. if source == 'exc':
  632. log.debug("ToolFilm.Film.generate_positive_punched_film() with Excellon source started ...")
  633. try:
  634. exc_name = self.exc_combo.currentText()
  635. except Exception:
  636. self.app.inform.emit('[ERROR_NOTCL] %s' %
  637. _("No Excellon object selected. Load an object for punching reference and retry."))
  638. return
  639. exc_obj = self.app.collection.get_by_name(exc_name)
  640. exc_solid_geometry = MultiPolygon(exc_obj.solid_geometry)
  641. punched_solid_geometry = MultiPolygon(film_obj.solid_geometry).difference(exc_solid_geometry)
  642. def init_func(new_obj, app_obj):
  643. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  644. outname = name + "_punched"
  645. self.app.new_object('gerber', outname, init_func)
  646. self.generate_positive_normal_film(outname, boxname, factor=factor, ftype=ftype)
  647. else:
  648. log.debug("ToolFilm.Film.generate_positive_punched_film() with Pad center source started ...")
  649. punch_size = float(self.punch_size_spinner.get_value())
  650. punching_geo = list()
  651. for apid in film_obj.apertures:
  652. if film_obj.apertures[apid]['type'] == 'C':
  653. if punch_size >= float(film_obj.apertures[apid]['size']):
  654. self.app.inform.emit('[ERROR_NOTCL] %s' %
  655. _(" Could not generate punched hole film because the punch hole size"
  656. "is bigger than some of the apertures in the Gerber object."))
  657. return 'fail'
  658. else:
  659. for elem in film_obj.apertures[apid]['geometry']:
  660. if 'follow' in elem:
  661. if isinstance(elem['follow'], Point):
  662. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  663. else:
  664. if punch_size >= float(film_obj.apertures[apid]['width']) or \
  665. punch_size >= float(film_obj.apertures[apid]['height']):
  666. self.app.inform.emit('[ERROR_NOTCL] %s' %
  667. _("Could not generate punched hole film because the punch hole size"
  668. "is bigger than some of the apertures in the Gerber object."))
  669. return 'fail'
  670. else:
  671. for elem in film_obj.apertures[apid]['geometry']:
  672. if 'follow' in elem:
  673. if isinstance(elem['follow'], Point):
  674. punching_geo.append(elem['follow'].buffer(punch_size / 2))
  675. punching_geo = MultiPolygon(punching_geo)
  676. if not isinstance(film_obj.solid_geometry, Polygon):
  677. temp_solid_geometry = MultiPolygon(film_obj.solid_geometry)
  678. else:
  679. temp_solid_geometry = film_obj.solid_geometry
  680. punched_solid_geometry = temp_solid_geometry.difference(punching_geo)
  681. if punched_solid_geometry == temp_solid_geometry:
  682. self.app.inform.emit('[WARNING_NOTCL] %s' %
  683. _("Could not generate punched hole film because the newly created object geometry "
  684. "is the same as the one in the source object geometry..."))
  685. return 'fail'
  686. def init_func(new_obj, app_obj):
  687. new_obj.solid_geometry = deepcopy(punched_solid_geometry)
  688. outname = name + "_punched"
  689. self.app.new_object('gerber', outname, init_func)
  690. self.generate_positive_normal_film(outname, boxname, factor=factor, ftype=ftype)
  691. def generate_negative_film(self, name, boxname, factor, ftype='svg'):
  692. log.debug("ToolFilm.Film.generate_negative_film() started ...")
  693. scale_factor_x = None
  694. scale_factor_y = None
  695. skew_factor_x = None
  696. skew_factor_y = None
  697. mirror = None
  698. skew_reference = 'center'
  699. if self.film_scale_cb.get_value():
  700. if self.film_scalex_entry.get_value() != 1.0:
  701. scale_factor_x = self.film_scalex_entry.get_value()
  702. if self.film_scaley_entry.get_value() != 1.0:
  703. scale_factor_y = self.film_scaley_entry.get_value()
  704. if self.film_skew_cb.get_value():
  705. if self.film_skewx_entry.get_value() != 0.0:
  706. skew_factor_x = self.film_skewx_entry.get_value()
  707. if self.film_skewy_entry.get_value() != 0.0:
  708. skew_factor_y = self.film_skewy_entry.get_value()
  709. skew_reference = self.film_skew_reference.get_value()
  710. if self.film_mirror_cb.get_value():
  711. if self.film_mirror_axis.get_value() != 'none':
  712. mirror = self.film_mirror_axis.get_value()
  713. border = float(self.boundary_entry.get_value())
  714. if border is None:
  715. border = 0
  716. if ftype == 'svg':
  717. filter_ext = "SVG Files (*.SVG);;"\
  718. "All Files (*.*)"
  719. elif ftype == 'png':
  720. filter_ext = "PNG Files (*.PNG);;" \
  721. "All Files (*.*)"
  722. else:
  723. filter_ext = "PDF Files (*.PDF);;" \
  724. "All Files (*.*)"
  725. try:
  726. filename, _f = QtWidgets.QFileDialog.getSaveFileName(
  727. caption=_("Export negative film"),
  728. directory=self.app.get_last_save_folder() + '/' + name + '_film',
  729. filter=filter_ext)
  730. except TypeError:
  731. filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export negative film"))
  732. filename = str(filename)
  733. if str(filename) == "":
  734. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export negative film cancelled."))
  735. return
  736. else:
  737. self.export_negative(name, boxname, filename, border,
  738. scale_stroke_factor=factor,
  739. scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
  740. skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
  741. skew_reference=skew_reference,
  742. mirror=mirror, ftype=ftype
  743. )
  744. def export_negative(self, obj_name, box_name, filename, boundary,
  745. scale_stroke_factor=0.00,
  746. scale_factor_x=None, scale_factor_y=None,
  747. skew_factor_x=None, skew_factor_y=None, skew_reference='center',
  748. mirror=None,
  749. use_thread=True, ftype='svg'):
  750. """
  751. Exports a Geometry Object to an SVG file in negative.
  752. :param obj_name: the name of the FlatCAM object to be saved as SVG
  753. :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
  754. :param filename: Path to the SVG file to save to.
  755. :param boundary: thickness of a black border to surround all the features
  756. :param scale_stroke_factor: factor by which to change/scale the thickness of the features
  757. :param scale_factor_x: factor to scale the svg geometry on the X axis
  758. :param scale_factor_y: factor to scale the svg geometry on the Y axis
  759. :param skew_factor_x: factor to skew the svg geometry on the X axis
  760. :param skew_factor_y: factor to skew the svg geometry on the Y axis
  761. :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
  762. those are the 4 points of the bounding box of the geometry to be skewed.
  763. :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
  764. :param use_thread: if to be run in a separate thread; boolean
  765. :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
  766. :return:
  767. """
  768. self.app.report_usage("export_negative()")
  769. if filename is None:
  770. filename = self.app.defaults["global_last_save_folder"]
  771. self.app.log.debug("export_svg() negative")
  772. try:
  773. obj = self.app.collection.get_by_name(str(obj_name))
  774. except Exception:
  775. # TODO: The return behavior has not been established... should raise exception?
  776. return "Could not retrieve object: %s" % obj_name
  777. try:
  778. box = self.app.collection.get_by_name(str(box_name))
  779. except Exception:
  780. # TODO: The return behavior has not been established... should raise exception?
  781. return "Could not retrieve object: %s" % box_name
  782. if box is None:
  783. self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
  784. box = obj
  785. def make_negative_film():
  786. exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
  787. scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
  788. skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
  789. mirror=mirror
  790. )
  791. # Determine bounding area for svg export
  792. bounds = box.bounds()
  793. size = box.size()
  794. uom = obj.units.lower()
  795. # Convert everything to strings for use in the xml doc
  796. svgwidth = str(size[0] + (2 * boundary))
  797. svgheight = str(size[1] + (2 * boundary))
  798. minx = str(bounds[0] - boundary)
  799. miny = str(bounds[1] + boundary + size[1])
  800. miny_rect = str(bounds[1] - boundary)
  801. # Add a SVG Header and footer to the svg output from shapely
  802. # The transform flips the Y Axis so that everything renders
  803. # properly within svg apps such as inkscape
  804. svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
  805. 'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
  806. svg_header += 'width="' + svgwidth + uom + '" '
  807. svg_header += 'height="' + svgheight + uom + '" '
  808. svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
  809. svg_header += '>'
  810. svg_header += '<g transform="scale(1,-1)">'
  811. svg_footer = '</g> </svg>'
  812. # Change the attributes of the exported SVG
  813. # We don't need stroke-width - wrong, we do when we have lines with certain width
  814. # We set opacity to maximum
  815. # We set the color to WHITE
  816. root = ET.fromstring(exported_svg)
  817. for child in root:
  818. child.set('fill', '#FFFFFF')
  819. child.set('opacity', '1.0')
  820. child.set('stroke', '#FFFFFF')
  821. # first_svg_elem = 'rect x="' + minx + '" ' + 'y="' + miny_rect + '" '
  822. # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
  823. # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
  824. first_svg_elem_tag = 'rect'
  825. first_svg_elem_attribs = {
  826. 'x': minx,
  827. 'y': miny_rect,
  828. 'width': svgwidth,
  829. 'height': svgheight,
  830. 'id': 'neg_rect',
  831. 'style': 'fill:#000000;opacity:1.0;stroke-width:0.0'
  832. }
  833. root.insert(0, ET.Element(first_svg_elem_tag, first_svg_elem_attribs))
  834. exported_svg = ET.tostring(root)
  835. svg_elem = svg_header + str(exported_svg) + svg_footer
  836. # Parse the xml through a xml parser just to add line feeds
  837. # and to make it look more pretty for the output
  838. doc = parse_xml_string(svg_elem)
  839. doc_final = doc.toprettyxml()
  840. if ftype == 'svg':
  841. try:
  842. with open(filename, 'w') as fp:
  843. fp.write(doc_final)
  844. except PermissionError:
  845. self.app.inform.emit('[WARNING] %s' %
  846. _("Permission denied, saving not possible.\n"
  847. "Most likely another app is holding the file open and not accessible."))
  848. return 'fail'
  849. elif ftype == 'png':
  850. try:
  851. doc_final = StringIO(doc_final)
  852. drawing = svg2rlg(doc_final)
  853. renderPM.drawToFile(drawing, filename, 'PNG')
  854. except Exception as e:
  855. log.debug("FilmTool.export_negative() --> PNG output --> %s" % str(e))
  856. return 'fail'
  857. else:
  858. try:
  859. if self.units == 'INCH':
  860. unit = inch
  861. else:
  862. unit = mm
  863. doc_final = StringIO(doc_final)
  864. drawing = svg2rlg(doc_final)
  865. p_size = self.pagesize_combo.get_value()
  866. if p_size == 'Bounds':
  867. renderPDF.drawToFile(drawing, filename)
  868. else:
  869. if self.orientation_radio.get_value() == 'p':
  870. page_size = portrait(self.pagesize[p_size])
  871. else:
  872. page_size = landscape(self.pagesize[p_size])
  873. my_canvas = canvas.Canvas(filename, pagesize=page_size)
  874. my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
  875. renderPDF.draw(drawing, my_canvas, 0, 0)
  876. my_canvas.save()
  877. except Exception as e:
  878. log.debug("FilmTool.export_negative() --> PDF output --> %s" % str(e))
  879. return 'fail'
  880. if self.app.defaults["global_open_style"] is False:
  881. self.app.file_opened.emit("SVG", filename)
  882. self.app.file_saved.emit("SVG", filename)
  883. self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
  884. if use_thread is True:
  885. proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
  886. def job_thread_film(app_obj):
  887. try:
  888. make_negative_film()
  889. except Exception:
  890. proc.done()
  891. return
  892. proc.done()
  893. self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
  894. else:
  895. make_negative_film()
  896. def export_positive(self, obj_name, box_name, filename,
  897. scale_stroke_factor=0.00,
  898. scale_factor_x=None, scale_factor_y=None,
  899. skew_factor_x=None, skew_factor_y=None, skew_reference='center',
  900. mirror=None, orientation_val='p', pagesize_val='A4', color_val='black', opacity_val=1.0,
  901. use_thread=True, ftype='svg'):
  902. """
  903. Exports a Geometry Object to an SVG file in positive black.
  904. :param obj_name: the name of the FlatCAM object to be saved as SVG
  905. :param box_name: the name of the FlatCAM object to be used as delimitation of the content to be saved
  906. :param filename: Path to the SVG file to save to.
  907. :param scale_stroke_factor: factor by which to change/scale the thickness of the features
  908. :param scale_factor_x: factor to scale the svg geometry on the X axis
  909. :param scale_factor_y: factor to scale the svg geometry on the Y axis
  910. :param skew_factor_x: factor to skew the svg geometry on the X axis
  911. :param skew_factor_y: factor to skew the svg geometry on the Y axis
  912. :param skew_reference: reference to use for skew. Can be 'bottomleft', 'bottomright', 'topleft', 'topright' and
  913. those are the 4 points of the bounding box of the geometry to be skewed.
  914. :param mirror: can be 'x' or 'y' or 'both'. Axis on which to mirror the svg geometry
  915. :param use_thread: if to be run in a separate thread; boolean
  916. :param ftype: the type of file for saving the film: 'svg', 'png' or 'pdf'
  917. :return:
  918. """
  919. self.app.report_usage("export_positive()")
  920. if filename is None:
  921. filename = self.app.defaults["global_last_save_folder"]
  922. self.app.log.debug("export_svg() black")
  923. try:
  924. obj = self.app.collection.get_by_name(str(obj_name))
  925. except Exception:
  926. # TODO: The return behavior has not been established... should raise exception?
  927. return "Could not retrieve object: %s" % obj_name
  928. try:
  929. box = self.app.collection.get_by_name(str(box_name))
  930. except Exception:
  931. # TODO: The return behavior has not been established... should raise exception?
  932. return "Could not retrieve object: %s" % box_name
  933. if box is None:
  934. self.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), obj))
  935. box = obj
  936. p_size = pagesize_val
  937. orientation = orientation_val
  938. color = color_val
  939. transparency_level = opacity_val
  940. def make_positive_film(p_size, orientation, color, transparency_level):
  941. log.debug("FilmTool.export_positive().make_positive_film()")
  942. exported_svg = obj.export_svg(scale_stroke_factor=scale_stroke_factor,
  943. scale_factor_x=scale_factor_x, scale_factor_y=scale_factor_y,
  944. skew_factor_x=skew_factor_x, skew_factor_y=skew_factor_y,
  945. mirror=mirror
  946. )
  947. # Change the attributes of the exported SVG
  948. # We don't need stroke-width
  949. # We set opacity to maximum
  950. # We set the colour to WHITE
  951. root = ET.fromstring(exported_svg)
  952. for child in root:
  953. child.set('fill', str(color))
  954. child.set('opacity', str(transparency_level))
  955. child.set('stroke', str(color))
  956. exported_svg = ET.tostring(root)
  957. # Determine bounding area for svg export
  958. bounds = box.bounds()
  959. size = box.size()
  960. # This contain the measure units
  961. uom = obj.units.lower()
  962. # Define a boundary around SVG of about 1.0mm (~39mils)
  963. if uom in "mm":
  964. boundary = 1.0
  965. else:
  966. boundary = 0.0393701
  967. # Convert everything to strings for use in the xml doc
  968. svgwidth = str(size[0] + (2 * boundary))
  969. svgheight = str(size[1] + (2 * boundary))
  970. minx = str(bounds[0] - boundary)
  971. miny = str(bounds[1] + boundary + size[1])
  972. # Add a SVG Header and footer to the svg output from shapely
  973. # The transform flips the Y Axis so that everything renders
  974. # properly within svg apps such as inkscape
  975. svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
  976. 'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
  977. svg_header += 'width="' + svgwidth + uom + '" '
  978. svg_header += 'height="' + svgheight + uom + '" '
  979. svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
  980. svg_header += '>'
  981. svg_header += '<g transform="scale(1,-1)">'
  982. svg_footer = '</g> </svg>'
  983. svg_elem = str(svg_header) + str(exported_svg) + str(svg_footer)
  984. # Parse the xml through a xml parser just to add line feeds
  985. # and to make it look more pretty for the output
  986. doc = parse_xml_string(svg_elem)
  987. doc_final = doc.toprettyxml()
  988. if ftype == 'svg':
  989. try:
  990. with open(filename, 'w') as fp:
  991. fp.write(doc_final)
  992. except PermissionError:
  993. self.app.inform.emit('[WARNING] %s' %
  994. _("Permission denied, saving not possible.\n"
  995. "Most likely another app is holding the file open and not accessible."))
  996. return 'fail'
  997. elif ftype == 'png':
  998. try:
  999. doc_final = StringIO(doc_final)
  1000. drawing = svg2rlg(doc_final)
  1001. renderPM.drawToFile(drawing, filename, 'PNG')
  1002. except Exception as e:
  1003. log.debug("FilmTool.export_positive() --> PNG output --> %s" % str(e))
  1004. return 'fail'
  1005. else:
  1006. try:
  1007. if self.units == 'IN':
  1008. unit = inch
  1009. else:
  1010. unit = mm
  1011. doc_final = StringIO(doc_final)
  1012. drawing = svg2rlg(doc_final)
  1013. if p_size == 'Bounds':
  1014. renderPDF.drawToFile(drawing, filename)
  1015. else:
  1016. if orientation == 'p':
  1017. page_size = portrait(self.pagesize[p_size])
  1018. else:
  1019. page_size = landscape(self.pagesize[p_size])
  1020. my_canvas = canvas.Canvas(filename, pagesize=page_size)
  1021. my_canvas.translate(bounds[0] * unit, bounds[1] * unit)
  1022. renderPDF.draw(drawing, my_canvas, 0, 0)
  1023. my_canvas.save()
  1024. except Exception as e:
  1025. log.debug("FilmTool.export_positive() --> PDF output --> %s" % str(e))
  1026. return 'fail'
  1027. if self.app.defaults["global_open_style"] is False:
  1028. self.app.file_opened.emit("SVG", filename)
  1029. self.app.file_saved.emit("SVG", filename)
  1030. self.app.inform.emit('[success] %s: %s' % (_("Film file exported to"), filename))
  1031. if use_thread is True:
  1032. proc = self.app.proc_container.new(_("Generating Film ... Please wait."))
  1033. def job_thread_film(app_obj):
  1034. try:
  1035. make_positive_film(p_size=p_size, orientation=orientation, color=color,
  1036. transparency_level=transparency_level)
  1037. except Exception:
  1038. proc.done()
  1039. return
  1040. proc.done()
  1041. self.app.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
  1042. else:
  1043. make_positive_film(p_size=p_size, orientation=orientation, color=color,
  1044. transparency_level=transparency_level)
  1045. def reset_fields(self):
  1046. self.tf_object_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  1047. self.tf_box_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))