chiark / gitweb /
Fix another UTF-8 filename encoding bug. Add error output to the slice log so we...
[cura.git] / Cura / cura_sf / skeinforge_application / skeinforge_plugins / craft_plugins / export.py
1 """
2 This page is in the table of contents.
3 Export is a craft tool to pick an export plugin, add information to the file name, and delete comments.
4
5 The export manual page is at:
6 http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Export
7
8 ==Operation==
9 The default 'Activate Export' checkbox is on.  When it is on, the functions described below will work, when it is off, the functions will not be called.
10
11 ==Settings==
12 ===Add Descriptive Extension===
13 Default is off.
14
15 When selected, key profile values will be added as an extension to the gcode file.  For example:
16 test.04hx06w_03fill_2cx2r_33EL.gcode
17
18 would mean:
19
20 * . (Carve section.)
21 * 04h = 'Layer Height (mm):' 0.4
22 * x
23 * 06w = 0.6 width i.e. 0.4 times 'Edge Width over Height (ratio):' 1.5
24 * _ (Fill section.)
25 * 03fill = 'Infill Solidity (ratio):' 0.3
26 * _ (Multiply section; if there is one column and one row then this section is not shown.)
27 * 2c = 'Number of Columns (integer):' 2
28 * x
29 * 2r = 'Number of Rows (integer):' 2.
30 * _ (Speed section.)
31 * 33EL = 'Feed Rate (mm/s):' 33.0 and 'Flow Rate Setting (float):' 33.0.  If either value has a positive value after the decimal place then this is also shown, but if it is zero it is hidden.  Also, if the values differ (which they shouldn't with 5D volumetrics) then each should be displayed separately.  For example, 35.2E30L = 'Feed Rate (mm/s):' 35.2 and 'Flow Rate Setting (float):' 30.0.
32
33 ===Add Profile Extension===
34 Default is off.
35
36 When selected, the current profile will be added to the file extension.  For example:
37 test.my_profile_name.gcode
38
39 ===Add Timestamp Extension===
40 Default is off.
41
42 When selected, the current date and time is added as an extension in format YYYYmmdd_HHMMSS (so it is sortable if one has many files).  For example:
43 test.my_profile_name.20110613_220113.gcode
44
45 ===Also Send Output To===
46 Default is empty.
47
48 Defines the output name for sending to a file or pipe.  A common choice is stdout to print the output in the shell screen.  Another common choice is stderr.  With the empty default, nothing will be done.  If the value is anything else, the output will be written to that file name.
49
50 ===Analyze Gcode===
51 Default is on.
52
53 When selected, the penultimate gcode will be sent to the analyze plugins to be analyzed and viewed.
54
55 ===Comment Choice===
56 Default is 'Delete All Comments'.
57
58 ====Do Not Delete Comments====
59 When selected, export will not delete comments.  Crafting comments slow down the processing in many firmware types, which leads to pauses and therefore a lower quality print.
60  
61 ====Delete Crafting Comments====
62 When selected, export will delete the time consuming crafting comments, but leave the initialization comments.  Since the crafting comments are deleted, there are no pauses during extrusion.  The remaining initialization comments provide some useful information for the analyze tools.
63
64 ====Delete All Comments====
65 When selected, export will delete all comments.  The comments are not necessary to run a fabricator.  Some printers do not support comments at all so the safest way is choose this option.
66
67 ===Export Operations===
68 Export presents the user with a choice of the export plugins in the export_plugins folder.  The chosen plugin will then modify the gcode or translate it into another format.  There is also the "Do Not Change Output" choice, which will not change the output.  An export plugin is a script in the export_plugins folder which has the getOutput function, the globalIsReplaceable variable and if it's output is not replaceable, the writeOutput function.
69
70 ===File Extension===
71 Default is gcode.
72
73 Defines the file extension added to the name of the output file.  The output file will be named as originalname_export.extension so if you are processing XYZ.stl the output will by default be XYZ_export.gcode
74  
75 ===Name of Replace File===
76 Default is replace.csv.
77
78 When export is exporting the code, if there is a tab separated file  with the name of the "Name of Replace File" setting, it will replace the string in the first column by its replacement in the second column.  If there is nothing in the second column, the first column string will be deleted, if this leads to an empty line, the line will be deleted.  If there are replacement columns after the second, they will be added as extra lines of text.  There is an example file replace_example.csv to demonstrate the tab separated format, which can be edited in a text editor or a spreadsheet.
79
80 Export looks for the alteration file in the alterations folder in the .skeinforge folder in the home directory.  Export does not care if the text file names are capitalized, but some file systems do not handle file name cases properly, so to be on the safe side you should give them lower case names.  If it doesn't find the file it then looks in the alterations folder in the skeinforge_plugins folder.
81
82 ===Save Penultimate Gcode===
83 Default is off.
84
85 When selected, export will save the gcode file with the suffix '_penultimate.gcode' just before it is exported.  This is useful because the code after it is exported could be in a form which the viewers can not display well.
86
87 ==Examples==
88 The following examples export the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and export.py.
89
90 > python export.py
91 This brings up the export dialog.
92
93 > python export.py Screw Holder Bottom.stl
94 The export tool is parsing the file:
95 Screw Holder Bottom.stl
96 ..
97 The export tool has created the file:
98 .. Screw Holder Bottom_export.gcode
99
100 """
101
102 from __future__ import absolute_import
103 #Init has to be imported first because it has code to workaround the python bug where relative imports don't work if the module is imported as a main module.
104 import __init__
105
106 from fabmetheus_utilities.fabmetheus_tools import fabmetheus_interpret
107 from fabmetheus_utilities import archive
108 from fabmetheus_utilities import euclidean
109 from fabmetheus_utilities import gcodec
110 from fabmetheus_utilities import intercircle
111 from fabmetheus_utilities import settings
112 from skeinforge_application.skeinforge_utilities import skeinforge_analyze
113 from skeinforge_application.skeinforge_utilities import skeinforge_craft
114 from skeinforge_application.skeinforge_utilities import skeinforge_polyfile
115 from skeinforge_application.skeinforge_utilities import skeinforge_profile
116 import cStringIO
117 import os
118 import sys
119 import time
120
121
122 __author__ = 'Enrique Perez (perez_enrique@yahoo.com)'
123 __credits__ = 'Gary Hodgson <http://garyhodgson.com/reprap/2011/06/hacking-skeinforge-export-module/>'
124 __date__ = '$Date: 2008/21/04 $'
125 __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'
126
127
128 def getCraftedTextFromText(gcodeText, repository=None):
129         'Export a gcode linear move text.'
130         if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'export'):
131                 return gcodeText
132         if repository == None:
133                 repository = settings.getReadRepository(ExportRepository())
134         if not repository.activateExport.value:
135                 return gcodeText
136         return ExportSkein().getCraftedGcode(repository, gcodeText)
137
138 def getDescriptionCarve(lines):
139         'Get the description for carve.'
140         descriptionCarve = ''
141         layerThicknessString = getSettingString(lines, 'carve', 'Layer Height')
142         if layerThicknessString != None:
143                 descriptionCarve += layerThicknessString.replace('.', '') + 'h'
144         edgeWidthString = getSettingString(lines, 'carve', 'Edge Width over Height')
145         if edgeWidthString != None:
146                 descriptionCarve += 'x%sw' % str(float(edgeWidthString) * float(layerThicknessString)).replace('.', '')
147         return descriptionCarve
148
149 def getDescriptionFill(lines):
150         'Get the description for fill.'
151         activateFillString = getSettingString(lines, 'fill', 'Activate Fill')
152         if activateFillString == None or activateFillString == 'False':
153                 return ''
154         infillSolidityString = getSettingString(lines, 'fill', 'Infill Solidity')
155         return '_' + infillSolidityString.replace('.', '') + 'fill'
156
157 def getDescriptionMultiply(lines):
158         'Get the description for multiply.'
159         activateMultiplyString = getSettingString(lines, 'multiply', 'Activate Multiply')
160         if activateMultiplyString == None or activateMultiplyString == 'False':
161                 return ''
162         columnsString = getSettingString(lines, 'multiply', 'Number of Columns')
163         rowsString = getSettingString(lines, 'multiply', 'Number of Rows')
164         if columnsString == '1' and rowsString == '1':
165                 return ''
166         return '_%scx%sr' % (columnsString, rowsString)
167
168 def getDescriptionSpeed(lines):
169         'Get the description for speed.'
170         activateSpeedString = getSettingString(lines, 'speed', 'Activate Speed')
171         if activateSpeedString == None or activateSpeedString == 'False':
172                 return ''
173         feedRateString = getSettingString(lines, 'speed', 'Feed Rate')
174         flowRateString = getSettingString(lines, 'speed', 'Flow Rate')
175         if feedRateString == flowRateString:
176                 return '_%sEL' % feedRateString.replace('.0', '')
177         return '_%sE%sL' % (feedRateString.replace('.0', ''), flowRateString.replace('.0', ''))
178
179 def getDescriptiveExtension(gcodeText):
180         'Get the descriptive extension.'
181         lines = archive.getTextLines(gcodeText)
182         return '.' + getDescriptionCarve(lines) + getDescriptionFill(lines) + getDescriptionMultiply(lines) + getDescriptionSpeed(lines)
183
184 def getDistanceGcode(exportText):
185         'Get gcode lines with distance variable added, this is for if ever there is distance code.'
186         lines = archive.getTextLines(exportText)
187         oldLocation = None
188         for line in lines:
189                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
190                 firstWord = None
191                 if len(splitLine) > 0:
192                         firstWord = splitLine[0]
193                 if firstWord == 'G1':
194                         location = gcodec.getLocationFromSplitLine(oldLocation, splitLine)
195                         if oldLocation != None:
196                                 distance = location.distance(oldLocation)
197                         oldLocation = location
198         return exportText
199
200 def getFirstValue(gcodeText, word):
201         'Get the value from the first line which starts with the given word.'
202         for line in archive.getTextLines(gcodeText):
203                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
204                 if gcodec.getFirstWord(splitLine) == word:
205                         return splitLine[1]
206         return ''
207
208 def getNewRepository():
209         'Get new repository.'
210         return ExportRepository()
211
212 def getReplaceableExportGcode(nameOfReplaceFile, replaceableExportGcode):
213         'Get text with strings replaced according to replace.csv file.'
214         replaceLines = settings.getAlterationLines(nameOfReplaceFile)
215         if len(replaceLines) < 1:
216                 return replaceableExportGcode
217         for replaceLine in replaceLines:
218                 splitLine = replaceLine.replace('\\n', '\t').split('\t')
219                 if len(splitLine) > 0:
220                         replaceableExportGcode = replaceableExportGcode.replace(splitLine[0], '\n'.join(splitLine[1 :]))
221         output = cStringIO.StringIO()
222         gcodec.addLinesToCString(output, archive.getTextLines(replaceableExportGcode))
223         return output.getvalue()
224
225 def getSelectedPluginModule( plugins ):
226         'Get the selected plugin module.'
227         for plugin in plugins:
228                 if plugin.value:
229                         return archive.getModuleWithDirectoryPath( plugin.directoryPath, plugin.name )
230         return None
231
232 def getSettingString(lines, procedureName, settingNameStart):
233         'Get the setting value from the lines, return None if there is no setting starting with that name.'
234         settingNameStart = settingNameStart.replace(' ', '_')
235         for line in lines:
236                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
237                 firstWord = None
238                 if len(splitLine) > 0:
239                         firstWord = splitLine[0]
240                 if firstWord == '(<setting>':
241                         if len(splitLine) > 4:
242                                 if splitLine[1] == procedureName and splitLine[2].startswith(settingNameStart):
243                                         return splitLine[3]
244                 elif firstWord == '(</settings>)':
245                         return None
246         return None
247
248 def sendOutputTo(outputTo, text):
249         'Send output to a file or a standard output.'
250         if outputTo.endswith('stderr'):
251                 sys.stderr.write(text)
252                 sys.stderr.write('\n')
253                 sys.stderr.flush()
254                 return
255         if outputTo.endswith('stdout'):
256                 sys.stdout.write(text)
257                 sys.stdout.write('\n')
258                 sys.stdout.flush()
259                 return
260         archive.writeFileText(outputTo, text)
261
262 def writeOutput(fileName, shouldAnalyze=True):
263         'Export a gcode linear move file.'
264         if fileName == '':
265                 return None
266         repository = ExportRepository()
267         settings.getReadRepository(repository)
268         startTime = time.time()
269         print('File ' + archive.getSummarizedFileName(fileName.encode('ascii', 'replace')) + ' is being chain exported.')
270         fileNameSuffix = fileName[: fileName.rfind('.')]
271         if repository.addExportSuffix.value:
272                 fileNameSuffix += '_export'
273         gcodeText = gcodec.getGcodeFileText(fileName, '')
274         procedures = skeinforge_craft.getProcedures('export', gcodeText)
275         gcodeText = skeinforge_craft.getChainTextFromProcedures(fileName, procedures[: -1], gcodeText)
276         if gcodeText == '':
277                 return None
278         if repository.addProfileExtension.value:
279                 fileNameSuffix += '.' + getFirstValue(gcodeText, '(<profileName>')
280         if repository.addDescriptiveExtension.value:
281                 fileNameSuffix += getDescriptiveExtension(gcodeText)
282         if repository.addTimestampExtension.value:
283                 fileNameSuffix += '.' + getFirstValue(gcodeText, '(<timeStampPreface>')
284         fileNameSuffix += '.' + repository.fileExtension.value
285         fileNamePenultimate = fileName[: fileName.rfind('.')] + '_penultimate.gcode'
286         filePenultimateWritten = False
287         if repository.savePenultimateGcode.value:
288                 archive.writeFileText(fileNamePenultimate, gcodeText)
289                 filePenultimateWritten = True
290                 print('The penultimate file is saved as ' + archive.getSummarizedFileName(fileNamePenultimate))
291         exportGcode = getCraftedTextFromText(gcodeText, repository)
292         window = None
293         if shouldAnalyze and repository.analyzeGcode.value:
294                 window = skeinforge_analyze.writeOutput(fileName, fileNamePenultimate, fileNameSuffix, filePenultimateWritten, gcodeText)
295         replaceableExportGcode = None
296         selectedPluginModule = getSelectedPluginModule(repository.exportPlugins)
297         if selectedPluginModule == None:
298                 replaceableExportGcode = exportGcode
299         else:
300                 if selectedPluginModule.globalIsReplaceable:
301                         replaceableExportGcode = selectedPluginModule.getOutput(exportGcode)
302                 else:
303                         selectedPluginModule.writeOutput(fileNameSuffix, exportGcode)
304         if replaceableExportGcode != None:
305                 replaceableExportGcode = getReplaceableExportGcode(repository.nameOfReplaceFile.value, replaceableExportGcode)
306                 archive.writeFileText( fileNameSuffix, replaceableExportGcode )
307                 print('The exported file is saved as ' + archive.getSummarizedFileName(fileNameSuffix))
308         if repository.alsoSendOutputTo.value != '':
309                 if replaceableExportGcode == None:
310                         replaceableExportGcode = selectedPluginModule.getOutput(exportGcode)
311                 sendOutputTo(repository.alsoSendOutputTo.value, replaceableExportGcode)
312         print('It took %s to export the file.' % euclidean.getDurationString(time.time() - startTime))
313         return window
314
315
316 class ExportRepository:
317         'A class to handle the export settings.'
318         def __init__(self):
319                 'Set the default settings, execute title & settings fileName.'
320                 skeinforge_profile.addListsToCraftTypeRepository('skeinforge_application.skeinforge_plugins.craft_plugins.export.html', self)
321                 self.fileNameInput = settings.FileNameInput().getFromFileName( fabmetheus_interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Export', self, '')
322                 self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute('http://fabmetheus.crsndoo.com/wiki/index.php/Skeinforge_Export')
323                 self.activateExport = settings.BooleanSetting().getFromValue('Activate Export', self, True)
324                 self.addDescriptiveExtension = settings.BooleanSetting().getFromValue('Add Descriptive Extension', self, False)
325                 self.addExportSuffix = settings.BooleanSetting().getFromValue('Add Export Suffix', self, True)
326                 self.addProfileExtension = settings.BooleanSetting().getFromValue('Add Profile Extension', self, False)
327                 self.addTimestampExtension = settings.BooleanSetting().getFromValue('Add Timestamp Extension', self, False)
328                 self.alsoSendOutputTo = settings.StringSetting().getFromValue('Also Send Output To:', self, '')
329                 self.analyzeGcode = settings.BooleanSetting().getFromValue('Analyze Gcode', self, True)
330                 self.commentChoice = settings.MenuButtonDisplay().getFromName('Comment Choice:', self)
331                 self.doNotDeleteComments = settings.MenuRadio().getFromMenuButtonDisplay(self.commentChoice, 'Do Not Delete Comments', self, True)
332                 self.deleteCraftingComments = settings.MenuRadio().getFromMenuButtonDisplay(self.commentChoice, 'Delete Crafting Comments', self, False)
333                 self.deleteAllComments = settings.MenuRadio().getFromMenuButtonDisplay(self.commentChoice, 'Delete All Comments', self, False)
334                 exportPluginsFolderPath = archive.getAbsoluteFrozenFolderPath(archive.getCraftPluginsDirectoryPath('export.py'), 'export_plugins')
335                 exportStaticDirectoryPath = os.path.join(exportPluginsFolderPath, 'static_plugins')
336                 exportPluginFileNames = archive.getPluginFileNamesFromDirectoryPath(exportPluginsFolderPath)
337                 exportStaticPluginFileNames = archive.getPluginFileNamesFromDirectoryPath(exportStaticDirectoryPath)
338                 self.exportLabel = settings.LabelDisplay().getFromName('Export Operations: ', self)
339                 self.exportPlugins = []
340                 exportLatentStringVar = settings.LatentStringVar()
341                 self.doNotChangeOutput = settings.RadioCapitalized().getFromRadio(exportLatentStringVar, 'Do Not Change Output', self, False)
342                 self.doNotChangeOutput.directoryPath = None
343                 allExportPluginFileNames = exportPluginFileNames + exportStaticPluginFileNames
344                 for exportPluginFileName in allExportPluginFileNames:
345                         exportPlugin = None
346                         default = False
347                         if exportPluginFileName == "gcode_small":
348                                 default = True
349                         if exportPluginFileName in exportPluginFileNames:
350                                 path = os.path.join(exportPluginsFolderPath, exportPluginFileName)
351                                 exportPlugin = settings.RadioCapitalizedButton().getFromPath(exportLatentStringVar, exportPluginFileName, path, self, default)
352                                 exportPlugin.directoryPath = exportPluginsFolderPath
353                         else:
354                                 exportPlugin = settings.RadioCapitalized().getFromRadio(exportLatentStringVar, exportPluginFileName, self, default)
355                                 exportPlugin.directoryPath = exportStaticDirectoryPath
356                         self.exportPlugins.append(exportPlugin)
357                 self.fileExtension = settings.StringSetting().getFromValue('File Extension:', self, 'gcode')
358                 self.nameOfReplaceFile = settings.StringSetting().getFromValue('Name of Replace File:', self, 'replace.csv')
359                 self.savePenultimateGcode = settings.BooleanSetting().getFromValue('Save Penultimate Gcode', self, False)
360                 self.executeTitle = 'Export'
361
362         def execute(self):
363                 'Export button has been clicked.'
364                 fileNames = skeinforge_polyfile.getFileOrDirectoryTypesUnmodifiedGcode(self.fileNameInput.value, fabmetheus_interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled)
365                 for fileName in fileNames:
366                         writeOutput(fileName)
367
368
369 class ExportSkein:
370         'A class to export a skein of extrusions.'
371         def __init__(self):
372                 self.crafting = False
373                 self.decimalPlacesExported = 2
374                 self.output = cStringIO.StringIO()
375
376         def addLine(self, line):
377                 'Add a line of text and a newline to the output.'
378                 if line != '':
379                         self.output.write(line + '\n')
380
381         def getCraftedGcode( self, repository, gcodeText ):
382                 'Parse gcode text and store the export gcode.'
383                 self.repository = repository
384                 lines = archive.getTextLines(gcodeText)
385                 for line in lines:
386                         self.parseLine(line)
387                 return self.output.getvalue()
388
389         def getLineWithTruncatedNumber(self, character, line, splitLine):
390                 'Get a line with the number after the character truncated.'
391                 numberString = gcodec.getStringFromCharacterSplitLine(character, splitLine)
392                 if numberString == None:
393                         return line
394                 roundedNumberString = euclidean.getRoundedToPlacesString(self.decimalPlacesExported, float(numberString))
395                 return gcodec.getLineWithValueString(character, line, splitLine, roundedNumberString)
396
397         def parseLine(self, line):
398                 'Parse a gcode line.'
399                 splitLine = gcodec.getSplitLineBeforeBracketSemicolon(line)
400                 if len(splitLine) < 1:
401                         self.addLine(line)
402                         return
403                 firstWord = splitLine[0]
404                 if firstWord == '(</crafting>)':
405                         self.crafting = False
406                 elif firstWord == '(<decimalPlacesCarried>':
407                         self.decimalPlacesExported = int(splitLine[1]) - 1
408                 if self.repository.deleteAllComments.value or (self.repository.deleteCraftingComments.value and self.crafting):
409                         if firstWord[0] == '(':
410                                 return
411                         else:
412                                 line = line.split(';')[0].split('(')[0].strip()
413                 if firstWord == '(<crafting>)':
414                         self.crafting = True
415                 if firstWord == '(</extruderInitialization>)':
416                         self.addLine(gcodec.getTagBracketedProcedure('export'))
417                 if firstWord != 'G1' and firstWord != 'G2' and firstWord != 'G3' :
418                         self.addLine(line)
419                         return
420                 line = self.getLineWithTruncatedNumber('X', line, splitLine)
421                 line = self.getLineWithTruncatedNumber('Y', line, splitLine)
422                 line = self.getLineWithTruncatedNumber('Z', line, splitLine)
423                 line = self.getLineWithTruncatedNumber('I', line, splitLine)
424                 line = self.getLineWithTruncatedNumber('J', line, splitLine)
425                 line = self.getLineWithTruncatedNumber('R', line, splitLine)
426                 self.addLine(line)
427
428
429 def main():
430         'Display the export dialog.'
431         if len(sys.argv) > 1:
432                 writeOutput(' '.join(sys.argv[1 :]))
433         else:
434                 settings.startMainLoopFromConstructor(getNewRepository())
435
436 if __name__ == '__main__':
437         main()