OpenShot Video Editor  2.0.0
blender_listview.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file contains the blender file listview, used by the 3d animated titles screen
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 #
7 # @section LICENSE
8 #
9 # Copyright (c) 2008-2018 OpenShot Studios, LLC
10 # (http://www.openshotstudios.com). This file is part of
11 # OpenShot Video Editor (http://www.openshot.org), an open-source project
12 # dedicated to delivering high quality video editing and animation solutions
13 # to the world.
14 #
15 # OpenShot Video Editor is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # OpenShot Video Editor is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
27 #
28 
29 import codecs
30 import os
31 import uuid
32 import shutil
33 import subprocess
34 import re
35 import xml.dom.minidom as xml
36 import functools
37 
38 from PyQt5.QtCore import QSize, Qt, QEvent, QObject, QThread, pyqtSlot, pyqtSignal, QMetaObject, Q_ARG, QTimer
39 from PyQt5.QtGui import *
40 from PyQt5.QtWidgets import *
41 
42 from classes import info
43 from classes.logger import log
44 from classes import settings
45 from classes.query import File
46 from classes.app import get_app
47 from windows.models.blender_model import BlenderModel
48 
49 try:
50  import json
51 except ImportError:
52  import simplejson as json
53 
54 
55 ##
56 # A custom Blender QEvent, which can safely be sent from the Blender thread to the Qt thread (to communicate)
57 class QBlenderEvent(QEvent):
58 
59  def __init__(self, id, data=None, *args):
60  # Invoke parent init
61  QEvent.__init__(self, id)
62  self.data = data
63  self.id = id
64 
65 
66 ##
67 # A TreeView QWidget used on the animated title window
68 class BlenderListView(QListView):
69 
70  def currentChanged(self, selected, deselected):
71  # Get selected item
72  self.selected = selected
73  self.deselected = deselected
74 
75  # Get translation object
76  _ = self.app._tr
77 
78  # Clear existing settings
79  self.win.clear_effect_controls()
80 
81  # Get animation details
82  animation = self.get_animation_details()
83  self.selected_template = animation.get("service")
84 
85  # In newer versions of Qt, setting the model invokes the currentChanged signal,
86  # but the selection is -1. So, just do nothing here.
87  if not self.selected_template:
88  return
89 
90  # Assign a new unique id for each template selected
92 
93  # Loop through params
94  for param in animation.get("params",[]):
95  log.info(param["title"])
96 
97  # Is Hidden Param?
98  if param["name"] == "start_frame" or param["name"] == "end_frame":
99  # add value to dictionary
100  self.params[param["name"]] = int(param["default"])
101 
102  # skip to next param without rendering the controls
103  continue
104 
105  # Create Label
106  widget = None
107  label = QLabel()
108  label.setText(_(param["title"]))
109  label.setToolTip(_(param["title"]))
110 
111  if param["type"] == "spinner":
112  # add value to dictionary
113  self.params[param["name"]] = float(param["default"])
114 
115  # create spinner
116  widget = QDoubleSpinBox()
117  widget.setMinimum(float(param["min"]))
118  widget.setMaximum(float(param["max"]))
119  widget.setValue(float(param["default"]))
120  widget.setSingleStep(0.01)
121  widget.setToolTip(param["title"])
122  widget.valueChanged.connect(functools.partial(self.spinner_value_changed, param))
123 
124  elif param["type"] == "text":
125  # add value to dictionary
126  self.params[param["name"]] = _(param["default"])
127 
128  # create spinner
129  widget = QLineEdit()
130  widget.setText(_(param["default"]))
131  widget.textChanged.connect(functools.partial(self.text_value_changed, widget, param))
132 
133  elif param["type"] == "multiline":
134  # add value to dictionary
135  self.params[param["name"]] = _(param["default"])
136 
137  # create spinner
138  widget = QTextEdit()
139  widget.setText(_(param["default"]).replace("\\n", "\n"))
140  widget.textChanged.connect(functools.partial(self.text_value_changed, widget, param))
141 
142  elif param["type"] == "dropdown":
143  # add value to dictionary
144  self.params[param["name"]] = param["default"]
145 
146  # create spinner
147  widget = QComboBox()
148  widget.currentIndexChanged.connect(functools.partial(self.dropdown_index_changed, widget, param))
149 
150  # Add values to dropdown
151  if "project_files" in param["name"]:
152  # override files dropdown
153  param["values"] = {}
154  for file in File.filter():
155  if file.data["media_type"] in ("image", "video"):
156  (dirName, fileName) = os.path.split(file.data["path"])
157  (fileBaseName, fileExtension) = os.path.splitext(fileName)
158 
159  if fileExtension.lower() not in (".svg"):
160  param["values"][fileName] = "|".join((file.data["path"], str(file.data["height"]),
161  str(file.data["width"]), file.data["media_type"],
162  str(file.data["fps"]["num"] / file.data["fps"][
163  "den"])))
164 
165  # Add normal values
166  box_index = 0
167  for k, v in sorted(param["values"].items()):
168  # add dropdown item
169  widget.addItem(_(k), v)
170 
171  # select dropdown (if default)
172  if v == param["default"]:
173  widget.setCurrentIndex(box_index)
174  box_index = box_index + 1
175 
176  if not param["values"]:
177  widget.addItem(_("No Files Found"), "")
178  widget.setEnabled(False)
179 
180  elif param["type"] == "color":
181  # add value to dictionary
182  color = QColor(param["default"])
183  self.params[param["name"]] = [color.redF(), color.greenF(), color.blueF()]
184 
185  widget = QPushButton()
186  widget.setText("")
187  widget.setStyleSheet("background-color: {}".format(param["default"]))
188  widget.clicked.connect(functools.partial(self.color_button_clicked, widget, param))
189 
190  # Add Label and Widget to the form
191  if (widget and label):
192  self.win.settingsContainer.layout().addRow(label, widget)
193  elif (label):
194  self.win.settingsContainer.layout().addRow(label)
195 
196  # Enable interface
197  self.enable_interface()
198 
199  # Init slider values
200  self.init_slider_values()
201 
202  def spinner_value_changed(self, param, value):
203  self.params[param["name"]] = value
204  log.info(value)
205 
206  def text_value_changed(self, widget, param, value=None):
207  try:
208  # Attempt to load value from QTextEdit (i.e. multi-line)
209  if not value:
210  value = widget.toPlainText()
211  except:
212  pass
213  self.params[param["name"]] = value.replace("\n", "\\n")
214  log.info(value)
215 
216  def dropdown_index_changed(self, widget, param, index):
217  value = widget.itemData(index)
218  self.params[param["name"]] = value
219  log.info(value)
220 
221  def color_button_clicked(self, widget, param, index):
222  # Show color dialog
223  log.info('Animation param being changed: %s' % param["name"])
224  color_value = self.params[param["name"]]
225  log.info('Value of param: %s' % color_value)
226  currentColor = QColor("#FFFFFF")
227  if len(color_value) == 3:
228  log.info('Using previous color: %s' % color_value)
229  #currentColor = QColor(color_value[0], color_value[1], color_value[2])
230  currentColor.setRgbF(color_value[0], color_value[1], color_value[2])
231  newColor = QColorDialog.getColor(currentColor)
232  if newColor.isValid():
233  widget.setStyleSheet("background-color: {}".format(newColor.name()))
234  self.params[param["name"]] = [newColor.redF(), newColor.greenF(), newColor.blueF()]
235  log.info(newColor.name())
236 
237  ##
238  # Generate a new, unique folder name to contain Blender frames
240 
241  # Assign a new unique id for each template selected
242  self.unique_folder_name = str(uuid.uuid1())
243 
244  # Create a folder (if it does not exist)
245  if not os.path.exists(os.path.join(info.BLENDER_PATH, self.unique_folder_name)):
246  os.mkdir(os.path.join(info.BLENDER_PATH, self.unique_folder_name))
247 
248  ##
249  # Disable all controls on interface
250  def disable_interface(self, cursor=True):
251  self.win.btnRefresh.setEnabled(False)
252  self.win.sliderPreview.setEnabled(False)
253  self.win.buttonBox.setEnabled(False)
254 
255  # Show 'Wait' cursor
256  if cursor:
257  QApplication.setOverrideCursor(Qt.WaitCursor)
258 
259  ##
260  # Disable all controls on interface
261  def enable_interface(self):
262  self.win.btnRefresh.setEnabled(True)
263  self.win.sliderPreview.setEnabled(True)
264  self.win.buttonBox.setEnabled(True)
265 
266  # Restore normal cursor
267  QApplication.restoreOverrideCursor()
268 
269  ##
270  # Init the slider and preview frame label to the currently selected animation
272  log.info("init_slider_values")
273 
274  # Get current preview slider frame
275  preview_frame_number = self.win.sliderPreview.value()
276  length = int(self.params.get("end_frame", 1))
277 
278  # Get the animation speed (if any)
279  if not self.params.get("animation_speed"):
280  self.params["animation_speed"] = 1
281  else:
282  # Adjust length (based on animation speed multiplier)
283  length *= int(self.params["animation_speed"])
284 
285  # Update the preview slider
286  middle_frame = int(length / 2)
287 
288  self.win.sliderPreview.setMinimum(self.params.get("start_frame", 1))
289  self.win.sliderPreview.setMaximum(length)
290  self.win.sliderPreview.setValue(middle_frame)
291 
292  # Update preview label
293  self.win.lblFrame.setText("{}/{}".format(middle_frame, length))
294 
295  # Click the refresh button
296  self.btnRefresh_clicked(None)
297 
298  def btnRefresh_clicked(self, checked):
299 
300  # Render current frame
301  log.info("btnRefresh_clicked")
302  preview_frame_number = self.win.sliderPreview.value()
303  self.Render(preview_frame_number)
304 
305  def render_finished(self):
306  log.info("RENDER FINISHED!")
307 
308  # Add file to project
309  final_path = os.path.join(info.BLENDER_PATH, self.unique_folder_name, self.params["file_name"] + "%04d.png")
310  log.info(final_path)
311 
312  # Add to project files
313  self.win.add_file(final_path)
314 
315  # Enable the Render button again
316  self.win.close()
317 
318  def close_window(self):
319  log.info("CLOSING WINDOW")
320 
321  # Close window
322  self.close()
323 
324  def update_progress_bar(self, current_frame, current_part, max_parts):
325 
326  # update label and preview slider
327  self.win.sliderPreview.setValue(current_frame)
328 
329  length = int(self.params["end_frame"])
330  self.win.lblFrame.setText("{}/{}".format(current_frame, length))
331 
332  ##
333  # Get new value of preview slider, and start timer to Render frame
334  def sliderPreview_valueChanged(self, new_value):
335  log.info('sliderPreview_valueChanged: %s' % new_value)
336  if self.win.sliderPreview.isEnabled():
337  self.preview_timer.start()
338 
339  # Update preview label
340  preview_frame_number = new_value
341  length = int(self.params["end_frame"])
342  self.win.lblFrame.setText("{}/{}".format(preview_frame_number, length))
343 
344  ##
345  # Timer is ready to Render frame
347  log.info('preview_timer_onTimeout')
348  self.preview_timer.stop()
349 
350  # Update preview label
351  preview_frame_number = self.win.sliderPreview.value()
352 
353  # Render current frame
354  self.Render(preview_frame_number)
355 
356  ##
357  # Build a dictionary of all animation settings and properties from XML
359 
360  if not self.selected:
361  return {}
362  elif self.selected and self.selected.row() == -1:
363  return {}
364 
365  # Get all selected rows items
366  ItemRow = self.blender_model.model.itemFromIndex(self.selected).row()
367  animation_title = self.blender_model.model.item(ItemRow, 1).text()
368  xml_path = self.blender_model.model.item(ItemRow, 2).text()
369  service = self.blender_model.model.item(ItemRow, 3).text()
370 
371  # load xml effect file
372  xmldoc = xml.parse(xml_path)
373 
374  # Get list of params
375  animation = {"title": animation_title, "path": xml_path, "service": service, "params": []}
376  xml_params = xmldoc.getElementsByTagName("param")
377 
378  # Loop through params
379  for param in xml_params:
380  param_item = {}
381 
382  # Get details of param
383  if param.attributes["title"]:
384  param_item["title"] = param.attributes["title"].value
385 
386  if param.attributes["description"]:
387  param_item["description"] = param.attributes["description"].value
388 
389  if param.attributes["name"]:
390  param_item["name"] = param.attributes["name"].value
391 
392  if param.attributes["type"]:
393  param_item["type"] = param.attributes["type"].value
394 
395  if param.getElementsByTagName("min"):
396  param_item["min"] = param.getElementsByTagName("min")[0].childNodes[0].data
397 
398  if param.getElementsByTagName("max"):
399  param_item["max"] = param.getElementsByTagName("max")[0].childNodes[0].data
400 
401  if param.getElementsByTagName("step"):
402  param_item["step"] = param.getElementsByTagName("step")[0].childNodes[0].data
403 
404  if param.getElementsByTagName("digits"):
405  param_item["digits"] = param.getElementsByTagName("digits")[0].childNodes[0].data
406 
407  if param.getElementsByTagName("default"):
408  if param.getElementsByTagName("default")[0].childNodes:
409  param_item["default"] = param.getElementsByTagName("default")[0].childNodes[0].data
410  else:
411  param_item["default"] = ""
412 
413  param_item["values"] = {}
414  values = param.getElementsByTagName("value")
415  for value in values:
416  # Get list of values
417  name = ""
418  num = ""
419 
420  if value.attributes["name"]:
421  name = value.attributes["name"].value
422 
423  if value.attributes["num"]:
424  num = value.attributes["num"].value
425 
426  # add to parameter
427  param_item["values"][name] = num
428 
429  # Append param object to list
430  animation["params"].append(param_item)
431 
432  # Return animation dictionary
433  return animation
434 
435  def mousePressEvent(self, event):
436 
437  # Ignore event, propagate to parent
438  event.ignore()
439  super().mousePressEvent(event)
440 
441  def refresh_view(self):
442  self.blender_model.update_model()
443 
444  ##
445  # Return a dictionary of project related settings, needed by the Blender python script.
446  def get_project_params(self, is_preview=True):
447 
448  project = self.app.project
449  project_params = {}
450 
451  # Append on some project settings
452  project_params["fps"] = project.get(["fps"])
453  project_params["resolution_x"] = project.get(["width"])
454  project_params["resolution_y"] = project.get(["height"])
455 
456  if is_preview:
457  project_params["resolution_percentage"] = 50
458  else:
459  project_params["resolution_percentage"] = 100
460  project_params["quality"] = 100
461  project_params["file_format"] = "PNG"
462  if is_preview:
463  # preview mode - use offwhite background (i.e. horizon color)
464  project_params["color_mode"] = "RGB"
465  project_params["alpha_mode"] = "SKY"
466  else:
467  # render mode - transparent background
468  project_params["color_mode"] = "RGBA"
469  project_params["alpha_mode"] = "TRANSPARENT"
470  project_params["horizon_color"] = (0.57, 0.57, 0.57)
471  project_params["animation"] = True
472  project_params["output_path"] = os.path.join(info.BLENDER_PATH, self.unique_folder_name,
473  self.params["file_name"])
474 
475  # return the dictionary
476  return project_params
477 
478  ##
479  # Show a friendly error message regarding the blender executable or version.
480  def error_with_blender(self, version=None, command_output=None):
481  _ = self.app._tr
483 
484  version_message = ""
485  if version:
486  version_message = _("\n\nVersion Detected:\n{}").format(version)
487 
488  if command_output:
489  version_message = _("\n\nError Output:\n{}").format(command_output)
490 
491  # show error message
492  blender_version = "2.78"
493  # Handle exception
494  msg = QMessageBox()
495  msg.setText(_(
496  "Blender, the free open source 3D content creation suite is required for this action (http://www.blender.org).\n\nPlease check the preferences in OpenShot and be sure the Blender executable is correct. This setting should be the path of the 'blender' executable on your computer. Also, please be sure that it is pointing to Blender version {} or greater.\n\nBlender Path:\n{}{}").format(
497  blender_version, s.get("blender_command"), version_message))
498  msg.exec_()
499 
500  # Enable the Render button again
501  self.enable_interface()
502 
503  def inject_params(self, path, frame=None):
504  # determine if this is 'preview' mode?
505  is_preview = False
506  if frame:
507  # if a frame is passed in, we are in preview mode.
508  # This is used to turn the background color to off-white... instead of transparent
509  is_preview = True
510 
511  # prepare string to inject
512  user_params = "\n#BEGIN INJECTING PARAMS\n"
513  for k, v in self.params.items():
514  if type(v) == int or type(v) == float or type(v) == list or type(v) == bool:
515  user_params += "params['{}'] = {}\n".format(k, v)
516  if type(v) == str:
517  user_params += "params['{}'] = u'{}'\n".format(k, v.replace("'", r"\'"))
518 
519  for k, v in self.get_project_params(is_preview).items():
520  if type(v) == int or type(v) == float or type(v) == list or type(v) == bool:
521  user_params += "params['{}'] = {}\n".format(k, v)
522  if type(v) == str:
523  user_params += "params['{}'] = u'{}'\n".format(k, v.replace("'", r"\'").replace("\\", "\\\\"))
524  user_params += "#END INJECTING PARAMS\n"
525 
526  # Force the Frame to 1 frame (for previewing)
527  if frame:
528  user_params += "\n\n#ONLY RENDER 1 FRAME FOR PREVIEW\n"
529  user_params += "params['{}'] = {}\n".format("start_frame", frame)
530  user_params += "params['{}'] = {}\n".format("end_frame", frame)
531  user_params += "\n\n#END ONLY RENDER 1 FRAME FOR PREVIEW\n"
532 
533  # Open new temp .py file, and inject the user parameters
534  with open(path, 'r') as f:
535  script_body = f.read()
536 
537  # modify script variable
538  script_body = script_body.replace("#INJECT_PARAMS_HERE", user_params)
539 
540  # Write update script
541  with codecs.open(path, "w", encoding="UTF-8") as f:
542  f.write(script_body)
543 
544  def update_image(self, image_path):
545 
546  # get the pixbuf
547  image = QImage(image_path)
548  scaled_image = image.scaledToHeight(self.win.imgPreview.height(), Qt.SmoothTransformation);
549  pixmap = QPixmap.fromImage(scaled_image)
550  self.win.imgPreview.setPixmap(pixmap)
551 
552  ##
553  # Render an images sequence of the current template using Blender 2.62+ and the
554  # Blender Python API.
555  def Render(self, frame=None):
556 
557  # Enable the Render button again
558  self.disable_interface()
559 
560  # Init blender paths
561  blend_file_path = os.path.join(info.PATH, "blender", "blend", self.selected_template)
562  source_script = os.path.join(info.PATH, "blender", "scripts", self.selected_template.replace(".blend", ".py"))
563  target_script = os.path.join(info.BLENDER_PATH, self.unique_folder_name,
564  self.selected_template.replace(".blend", ".py"))
565 
566  # Copy the .py script associated with this template to the temp folder. This will allow
567  # OpenShot to inject the user-entered params into the Python script.
568  shutil.copy(source_script, target_script)
569 
570  # Open new temp .py file, and inject the user parameters
571  self.inject_params(target_script, frame)
572 
573  # Create new thread to launch the Blender executable (and read the output)
574  if frame:
575  # preview mode
576  QMetaObject.invokeMethod(self.worker, 'Render', Qt.QueuedConnection,
577  Q_ARG(str, blend_file_path),
578  Q_ARG(str, target_script),
579  Q_ARG(bool, True))
580  else:
581  # render mode
582  # self.my_blender = BlenderCommand(self, blend_file_path, target_script, False)
583  QMetaObject.invokeMethod(self.worker, 'Render', Qt.QueuedConnection,
584  Q_ARG(str, blend_file_path),
585  Q_ARG(str, target_script),
586  Q_ARG(bool, False))
587 
588  def __init__(self, *args):
589  # Invoke parent init
590  QTreeView.__init__(self, *args)
591 
592  # Get a reference to the window object
593  self.app = get_app()
594  self.win = args[0]
595 
596  # Get Model data
597  self.blender_model = BlenderModel()
598 
599  # Keep track of mouse press start position to determine when to start drag
600  self.selected = None
601  self.deselected = None
602 
603  # Preview render timer
604  self.preview_timer = QTimer(self)
605  self.preview_timer.setInterval(300)
606  self.preview_timer.timeout.connect(self.preview_timer_onTimeout)
607 
608  # Init dictionary which holds the values to the template parameters
609  self.params = {}
610 
611  # Assign a new unique id for each template selected
612  self.unique_folder_name = None
613 
614  # Disable interface
615  self.disable_interface(cursor=False)
616  self.selected_template = ""
617 
618  # Setup header columns
619  self.setModel(self.blender_model.model)
620  self.setIconSize(QSize(131, 108))
621  self.setGridSize(QSize(102, 92))
622  self.setViewMode(QListView.IconMode)
623  self.setResizeMode(QListView.Adjust)
624  self.setUniformItemSizes(False)
625  self.setWordWrap(True)
626  self.setTextElideMode(Qt.ElideRight)
627  self.setStyleSheet('QTreeView::item { padding-top: 2px; }')
628 
629  # Hook up button
630  self.win.btnRefresh.clicked.connect(functools.partial(self.btnRefresh_clicked))
631  self.win.sliderPreview.valueChanged.connect(functools.partial(self.sliderPreview_valueChanged))
632 
633  # Refresh view
634  self.refresh_view()
635 
636 
637  # Background Worker Thread (for Blender process)
638  self.background = QThread(self)
639  self.worker = Worker() # no parent!
640 
641  # Hook up signals to Background Worker
642  self.worker.closed.connect(self.onCloseWindow)
643  self.worker.finished.connect(self.onRenderFinish)
644  self.worker.blender_version_error.connect(self.onBlenderVersionError)
645  self.worker.blender_error_nodata.connect(self.onBlenderErrorNoData)
646  self.worker.progress.connect(self.onUpdateProgress)
647  self.worker.image_updated.connect(self.onUpdateImage)
648  self.worker.blender_error_with_data.connect(self.onBlenderErrorMessage)
649  self.worker.enable_interface.connect(self.onRenableInterface)
650 
651  # Move Worker to new thread, and Start
652  self.worker.moveToThread(self.background)
653  self.background.start()
654 
655  # Signal when to close window (1001)
656  def onCloseWindow(self):
657  log.info('onCloseWindow')
658  self.close()
659 
660  # Signal when render is finished (1002)
661  def onRenderFinish(self):
662  log.info('onRenderFinish')
663  self.render_finished()
664 
665  # Error from blender (with version number) (1003)
666  def onBlenderVersionError(self, version):
667  log.info('onBlenderVersionError')
668  self.error_with_blender(version)
669 
670  # Error from blender (with no data) (1004)
672  log.info('onBlenderErrorNoData')
673  self.error_with_blender()
674 
675  # Signal when to update progress bar (1005)
676  def onUpdateProgress(self, current_frame, current_part, max_parts):
677  # log.info ('onUpdateProgress')
678  self.update_progress_bar(current_frame, current_part, max_parts)
679 
680  # Signal when to update preview image (1006)
681  def onUpdateImage(self, image_path):
682  # log.info ('onUpdateImage: %s' % image_path)
683  self.update_image(image_path)
684 
685  # Signal error from blender (with custom message) (1007)
686  def onBlenderErrorMessage(self, error):
687  log.info('onBlenderErrorMessage')
688  self.error_with_blender(None, error)
689 
690  # Signal when to re-enable interface (1008)
692  log.info('onRenableInterface')
693  self.enable_interface()
694 
695 
696 ##
697 # Background Worker Object (to run the Blender commands)
698 class Worker(QObject):
699 
700  closed = pyqtSignal() # 1001
701  finished = pyqtSignal() # 1002
702  blender_version_error = pyqtSignal(str) # 1003
703  blender_error_nodata = pyqtSignal() # 1004
704  progress = pyqtSignal(int, int, int) # 1005
705  image_updated = pyqtSignal(str) # 1006
706  blender_error_with_data = pyqtSignal(str) # 1007
707  enable_interface = pyqtSignal() # 1008
708 
709  @pyqtSlot(str, str, bool)
710  ##
711  # Worker's Render method which invokes the Blender rendering commands
712  def Render(self, blend_file_path, target_script, preview_mode=False):
713  log.info("QThread Render Method Invoked")
714 
715  # Init regex expression used to determine blender's render progress
717 
718  # get the blender executable path
719  self.blender_exec_path = s.get("blender_command")
720  self.blender_frame_expression = re.compile(r"Fra:([0-9,]*).*Mem:(.*?) .*Part ([0-9,]*)-([0-9,]*)")
721  self.blender_saved_expression = re.compile(r"Saved: '(.*.png)(.*)'")
722  self.blender_version = re.compile(r"Blender (.*?) ")
723  self.blend_file_path = blend_file_path
724  self.target_script = target_script
725  self.preview_mode = preview_mode
726  self.frame_detected = False
727  self.version = None
728  self.command_output = ""
729  self.process = None
730  self.is_running = True
731  _ = get_app()._tr
732 
733  try:
734  # Shell the blender command to create the image sequence
735  command_get_version = [self.blender_exec_path, '-v']
736  command_render = [self.blender_exec_path, '-b', self.blend_file_path, '-P', self.target_script]
737  self.process = subprocess.Popen(command_get_version, stdout=subprocess.PIPE)
738 
739  # Check the version of Blender
740  self.version = self.blender_version.findall(str(self.process.stdout.readline()))
741 
742  if self.version:
743  if float(self.version[0]) < 2.78:
744  # change cursor to "default" and stop running blender command
745  self.is_running = False
746 
747  # Wrong version of Blender. Must be 2.62+:
748  self.blender_version_error.emit(float(self.version[0]))
749  return
750 
751  # debug info
752  log.info(
753  "Blender command: {} {} '{}' {} '{}'".format(command_render[0], command_render[1], command_render[2],
754  command_render[3], command_render[4]))
755 
756  # Run real command to render Blender project
757  self.process = subprocess.Popen(command_render, stdout=subprocess.PIPE)
758 
759  except:
760  # Error running command. Most likely the blender executable path in the settings
761  # is not correct, or is not the correct version of Blender (i.e. 2.62+)
762  self.is_running = False
763  self.blender_error_nodata.emit()
764  return
765 
766  while self.is_running and self.process.poll() is None:
767 
768  # Look for progress info in the Blender Output
769  line = str(self.process.stdout.readline())
770  self.command_output = self.command_output + line + "\n" # append all output into a variable
771  output_frame = self.blender_frame_expression.findall(line)
772 
773  # Does it have a match?
774  if output_frame:
775  # Yes, we have a match
776  self.frame_detected = True
777  current_frame = output_frame[0][0]
778  memory = output_frame[0][1]
779  current_part = output_frame[0][2]
780  max_parts = output_frame[0][3]
781 
782  # Update progress bar
783  if not self.preview_mode:
784  # only update progress if in 'render' mode
785  self.progress.emit(float(current_frame), float(current_part), float(max_parts))
786 
787  # Look for progress info in the Blender Output
788  output_saved = self.blender_saved_expression.findall(str(line))
789  log.info("Image detected from blender regex: %s" % output_saved)
790 
791  # Does it have a match?
792  if output_saved:
793  # Yes, we have a match
794  self.frame_detected = True
795  image_path = output_saved[0][0]
796  time_saved = output_saved[0][1]
797 
798  # Update preview image
799  self.image_updated.emit(image_path)
800 
801 
802  # Re-enable the interface
803  self.enable_interface.emit()
804 
805  # Check if NO FRAMES are detected
806  if not self.frame_detected:
807  # Show Error that no frames are detected. This is likely caused by
808  # the wrong command being executed... or an error in Blender.
809  self.blender_error_with_data.emit(_("No frame was found in the output from Blender"))
810 
811  # Done with render (i.e. close window)
812  elif not self.preview_mode:
813  # only close window if in 'render' mode
814  self.finished.emit()
815 
816  # Thread finished
817  log.info("Blender render thread finished")
818  if self.is_running == False:
819  # close window if thread was killed
820  self.closed.emit()
821 
822  # mark thread as finished
823  self.is_running = False
def update_progress_bar(self, current_frame, current_part, max_parts)
def currentChanged(self, selected, deselected)
def color_button_clicked(self, widget, param, index)
def onUpdateImage(self, image_path)
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def Render(self, frame=None)
Render an images sequence of the current template using Blender 2.62+ and the Blender Python API...
def __init__(self, id, data=None, args)
def update_image(self, image_path)
A TreeView QWidget used on the animated title window.
def spinner_value_changed(self, param, value)
def dropdown_index_changed(self, widget, param, index)
def preview_timer_onTimeout(self)
Timer is ready to Render frame.
def generateUniqueFolder(self)
Generate a new, unique folder name to contain Blender frames.
def disable_interface(self, cursor=True)
Disable all controls on interface.
def enable_interface(self)
Disable all controls on interface.
def text_value_changed(self, widget, param, value=None)
def init_slider_values(self)
Init the slider and preview frame label to the currently selected animation.
A custom Blender QEvent, which can safely be sent from the Blender thread to the Qt thread (to commun...
def sliderPreview_valueChanged(self, new_value)
Get new value of preview slider, and start timer to Render frame.
def Render(self, blend_file_path, target_script, preview_mode=False)
Worker&#39;s Render method which invokes the Blender rendering commands.
def get_animation_details(self)
Build a dictionary of all animation settings and properties from XML.
def get_settings()
Get the current QApplication&#39;s settings instance.
Definition: settings.py:44
def get_project_params(self, is_preview=True)
Return a dictionary of project related settings, needed by the Blender python script.
def error_with_blender(self, version=None, command_output=None)
Show a friendly error message regarding the blender executable or version.
Background Worker Object (to run the Blender commands)
def onBlenderVersionError(self, version)
def inject_params(self, path, frame=None)
def onUpdateProgress(self, current_frame, current_part, max_parts)