OpenShot Video Editor  2.0.0
blender_treeview.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file contains the blender file treeview, used by the 3d animated titles screen
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 #
7 # @section LICENSE
8 #
9 # Copyright (c) 2008-2016 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 BlenderTreeView(QTreeView):
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 contextMenuEvent(self, event):
436  menu = QMenu(self)
437  menu.addAction(self.win.actionDetailsView)
438  menu.addAction(self.win.actionThumbnailView)
439  menu.exec_(QCursor.pos())
440 
441  # Ignore event, propagate to parent
442  event.ignore()
443  super().mouseMoveEvent(event)
444 
445  def mousePressEvent(self, event):
446 
447  # Ignore event, propagate to parent
448  event.ignore()
449  super().mousePressEvent(event)
450 
451  def refresh_view(self):
452  self.blender_model.update_model()
453  self.hideColumn(2)
454  self.hideColumn(3)
455 
456  ##
457  # Return a dictionary of project related settings, needed by the Blender python script.
458  def get_project_params(self, is_preview=True):
459 
460  project = self.app.project
461  project_params = {}
462 
463  # Append on some project settings
464  project_params["fps"] = project.get(["fps"])
465  project_params["resolution_x"] = project.get(["width"])
466  project_params["resolution_y"] = project.get(["height"])
467 
468  if is_preview:
469  project_params["resolution_percentage"] = 50
470  else:
471  project_params["resolution_percentage"] = 100
472  project_params["quality"] = 100
473  project_params["file_format"] = "PNG"
474  if is_preview:
475  # preview mode - use offwhite background (i.e. horizon color)
476  project_params["color_mode"] = "RGB"
477  else:
478  # render mode - transparent background
479  project_params["color_mode"] = "RGBA"
480  project_params["horizon_color"] = (0.57, 0.57, 0.57)
481  project_params["animation"] = True
482  project_params["output_path"] = os.path.join(info.BLENDER_PATH, self.unique_folder_name,
483  self.params["file_name"])
484 
485  # return the dictionary
486  return project_params
487 
488  ##
489  # Show a friendly error message regarding the blender executable or version.
490  def error_with_blender(self, version=None, command_output=None):
491  _ = self.app._tr
493 
494  version_message = ""
495  if version:
496  version_message = _("\n\nVersion Detected:\n{}").format(version)
497 
498  if command_output:
499  version_message = _("\n\nError Output:\n{}").format(command_output)
500 
501  # show error message
502  blender_version = "2.62"
503  # Handle exception
504  msg = QMessageBox()
505  msg.setText(_(
506  "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(
507  blender_version, s.get("blender_command"), version_message))
508  msg.exec_()
509 
510  # Enable the Render button again
511  self.enable_interface()
512 
513  def inject_params(self, path, frame=None):
514  # determine if this is 'preview' mode?
515  is_preview = False
516  if frame:
517  # if a frame is passed in, we are in preview mode.
518  # This is used to turn the background color to off-white... instead of transparent
519  is_preview = True
520 
521  # prepare string to inject
522  user_params = "\n#BEGIN INJECTING PARAMS\n"
523  for k, v in self.params.items():
524  if type(v) == int or type(v) == float or type(v) == list or type(v) == bool:
525  user_params += "params['{}'] = {}\n".format(k, v)
526  if type(v) == str:
527  user_params += "params['{}'] = u'{}'\n".format(k, v.replace("'", r"\'"))
528 
529  for k, v in self.get_project_params(is_preview).items():
530  if type(v) == int or type(v) == float or type(v) == list or type(v) == bool:
531  user_params += "params['{}'] = {}\n".format(k, v)
532  if type(v) == str:
533  user_params += "params['{}'] = u'{}'\n".format(k, v.replace("'", r"\'").replace("\\", "\\\\"))
534  user_params += "#END INJECTING PARAMS\n"
535 
536  # Force the Frame to 1 frame (for previewing)
537  if frame:
538  user_params += "\n\n#ONLY RENDER 1 FRAME FOR PREVIEW\n"
539  user_params += "params['{}'] = {}\n".format("start_frame", frame)
540  user_params += "params['{}'] = {}\n".format("end_frame", frame)
541  user_params += "\n\n#END ONLY RENDER 1 FRAME FOR PREVIEW\n"
542 
543  # Open new temp .py file, and inject the user parameters
544  with open(path, 'r') as f:
545  script_body = f.read()
546 
547  # modify script variable
548  script_body = script_body.replace("#INJECT_PARAMS_HERE", user_params)
549 
550  # Write update script
551  with codecs.open(path, "w", encoding="UTF-8") as f:
552  f.write(script_body)
553 
554  def update_image(self, image_path):
555 
556  # get the pixbuf
557  image = QImage(image_path)
558  scaled_image = image.scaledToHeight(self.win.imgPreview.height(), Qt.SmoothTransformation);
559  pixmap = QPixmap.fromImage(scaled_image)
560  self.win.imgPreview.setPixmap(pixmap)
561 
562  ##
563  # Render an images sequence of the current template using Blender 2.62+ and the
564  # Blender Python API.
565  def Render(self, frame=None):
566 
567  # Enable the Render button again
568  self.disable_interface()
569 
570  # Init blender paths
571  blend_file_path = os.path.join(info.PATH, "blender", "blend", self.selected_template)
572  source_script = os.path.join(info.PATH, "blender", "scripts", self.selected_template.replace(".blend", ".py"))
573  target_script = os.path.join(info.BLENDER_PATH, self.unique_folder_name,
574  self.selected_template.replace(".blend", ".py"))
575 
576  # Copy the .py script associated with this template to the temp folder. This will allow
577  # OpenShot to inject the user-entered params into the Python script.
578  shutil.copy(source_script, target_script)
579 
580  # Open new temp .py file, and inject the user parameters
581  self.inject_params(target_script, frame)
582 
583  # Create new thread to launch the Blender executable (and read the output)
584  if frame:
585  # preview mode
586  QMetaObject.invokeMethod(self.worker, 'Render', Qt.QueuedConnection,
587  Q_ARG(str, blend_file_path),
588  Q_ARG(str, target_script),
589  Q_ARG(bool, True))
590  else:
591  # render mode
592  # self.my_blender = BlenderCommand(self, blend_file_path, target_script, False)
593  QMetaObject.invokeMethod(self.worker, 'Render', Qt.QueuedConnection,
594  Q_ARG(str, blend_file_path),
595  Q_ARG(str, target_script),
596  Q_ARG(bool, False))
597 
598  def __init__(self, *args):
599  # Invoke parent init
600  QTreeView.__init__(self, *args)
601 
602  # Get a reference to the window object
603  self.app = get_app()
604  self.win = args[0]
605 
606  # Get Model data
607  self.blender_model = BlenderModel()
608 
609  # Keep track of mouse press start position to determine when to start drag
610  self.selected = None
611  self.deselected = None
612 
613  # Preview render timer
614  self.preview_timer = QTimer(self)
615  self.preview_timer.setInterval(300)
616  self.preview_timer.timeout.connect(self.preview_timer_onTimeout)
617 
618  # Init dictionary which holds the values to the template parameters
619  self.params = {}
620 
621  # Assign a new unique id for each template selected
622  self.unique_folder_name = None
623 
624  # Disable interface
625  self.disable_interface(cursor=False)
626  self.selected_template = ""
627 
628  # Setup header columns
629  self.setModel(self.blender_model.model)
630  self.setIconSize(QSize(131, 108))
631  self.setIndentation(0)
632  self.setSelectionBehavior(QTreeView.SelectRows)
633  self.setSelectionBehavior(QAbstractItemView.SelectRows)
634  self.setWordWrap(True)
635  self.setStyleSheet('QTreeView::item { padding-top: 2px; }')
636 
637  # Hook up button
638  self.win.btnRefresh.clicked.connect(functools.partial(self.btnRefresh_clicked))
639  self.win.sliderPreview.valueChanged.connect(functools.partial(self.sliderPreview_valueChanged))
640 
641  # Refresh view
642  self.refresh_view()
643 
644 
645  # Background Worker Thread (for Blender process)
646  self.background = QThread(self)
647  self.worker = Worker() # no parent!
648 
649  # Hook up signals to Background Worker
650  self.worker.closed.connect(self.onCloseWindow)
651  self.worker.finished.connect(self.onRenderFinish)
652  self.worker.blender_version_error.connect(self.onBlenderVersionError)
653  self.worker.blender_error_nodata.connect(self.onBlenderErrorNoData)
654  self.worker.progress.connect(self.onUpdateProgress)
655  self.worker.image_updated.connect(self.onUpdateImage)
656  self.worker.blender_error_with_data.connect(self.onBlenderErrorMessage)
657  self.worker.enable_interface.connect(self.onRenableInterface)
658 
659  # Move Worker to new thread, and Start
660  self.worker.moveToThread(self.background)
661  self.background.start()
662 
663  # Signal when to close window (1001)
664  def onCloseWindow(self):
665  log.info('onCloseWindow')
666  self.close()
667 
668  # Signal when render is finished (1002)
669  def onRenderFinish(self):
670  log.info('onRenderFinish')
671  self.render_finished()
672 
673  # Error from blender (with version number) (1003)
674  def onBlenderVersionError(self, version):
675  log.info('onBlenderVersionError')
676  self.error_with_blender(version)
677 
678  # Error from blender (with no data) (1004)
680  log.info('onBlenderErrorNoData')
681  self.error_with_blender()
682 
683  # Signal when to update progress bar (1005)
684  def onUpdateProgress(self, current_frame, current_part, max_parts):
685  # log.info ('onUpdateProgress')
686  self.update_progress_bar(current_frame, current_part, max_parts)
687 
688  # Signal when to update preview image (1006)
689  def onUpdateImage(self, image_path):
690  # log.info ('onUpdateImage: %s' % image_path)
691  self.update_image(image_path)
692 
693  # Signal error from blender (with custom message) (1007)
694  def onBlenderErrorMessage(self, error):
695  log.info('onBlenderErrorMessage')
696  self.error_with_blender(None, error)
697 
698  # Signal when to re-enable interface (1008)
700  log.info('onRenableInterface')
701  self.enable_interface()
702 
703 
704 ##
705 # Background Worker Object (to run the Blender commands)
706 class Worker(QObject):
707 
708  closed = pyqtSignal() # 1001
709  finished = pyqtSignal() # 1002
710  blender_version_error = pyqtSignal(str) # 1003
711  blender_error_nodata = pyqtSignal() # 1004
712  progress = pyqtSignal(int, int, int) # 1005
713  image_updated = pyqtSignal(str) # 1006
714  blender_error_with_data = pyqtSignal(str) # 1007
715  enable_interface = pyqtSignal() # 1008
716 
717  @pyqtSlot(str, str, bool)
718  ##
719  # Worker's Render method which invokes the Blender rendering commands
720  def Render(self, blend_file_path, target_script, preview_mode=False):
721  log.info("QThread Render Method Invoked")
722 
723  # Init regex expression used to determine blender's render progress
725 
726  # get the blender executable path
727  self.blender_exec_path = s.get("blender_command")
728  self.blender_frame_expression = re.compile(r"Fra:([0-9,]*).*Mem:(.*?) .*Part ([0-9,]*)-([0-9,]*)")
729  self.blender_saved_expression = re.compile(r"Saved: (.*.png)(.*)")
730  self.blender_version = re.compile(r"Blender (.*?) ")
731  self.blend_file_path = blend_file_path
732  self.target_script = target_script
733  self.preview_mode = preview_mode
734  self.frame_detected = False
735  self.version = None
736  self.command_output = ""
737  self.process = None
738  self.is_running = True
739  _ = get_app()._tr
740 
741  try:
742  # Shell the blender command to create the image sequence
743  command_get_version = [self.blender_exec_path, '-v']
744  command_render = [self.blender_exec_path, '-b', self.blend_file_path, '-P', self.target_script]
745  self.process = subprocess.Popen(command_get_version, stdout=subprocess.PIPE)
746 
747  # Check the version of Blender
748  self.version = self.blender_version.findall(str(self.process.stdout.readline()))
749 
750  if self.version:
751  if float(self.version[0]) < 2.62:
752  # change cursor to "default" and stop running blender command
753  self.is_running = False
754 
755  # Wrong version of Blender. Must be 2.62+:
756  self.blender_version_error.emit(float(self.version[0]))
757  return
758 
759  # debug info
760  log.info(
761  "Blender command: {} {} '{}' {} '{}'".format(command_render[0], command_render[1], command_render[2],
762  command_render[3], command_render[4]))
763 
764  # Run real command to render Blender project
765  self.process = subprocess.Popen(command_render, stdout=subprocess.PIPE)
766 
767  except:
768  # Error running command. Most likely the blender executable path in the settings
769  # is not correct, or is not the correct version of Blender (i.e. 2.62+)
770  self.is_running = False
771  self.blender_error_nodata.emit()
772  return
773 
774  while self.is_running and self.process.poll() is None:
775 
776  # Look for progress info in the Blender Output
777  line = str(self.process.stdout.readline())
778  self.command_output = self.command_output + line + "\n" # append all output into a variable
779  output_frame = self.blender_frame_expression.findall(line)
780 
781  # Does it have a match?
782  if output_frame:
783  # Yes, we have a match
784  self.frame_detected = True
785  current_frame = output_frame[0][0]
786  memory = output_frame[0][1]
787  current_part = output_frame[0][2]
788  max_parts = output_frame[0][3]
789 
790  # Update progress bar
791  if not self.preview_mode:
792  # only update progress if in 'render' mode
793  self.progress.emit(float(current_frame), float(current_part), float(max_parts))
794 
795  # Look for progress info in the Blender Output
796  output_saved = self.blender_saved_expression.findall(str(line))
797 
798  # Does it have a match?
799  if output_saved:
800  # Yes, we have a match
801  self.frame_detected = True
802  image_path = output_saved[0][0]
803  time_saved = output_saved[0][1]
804 
805  # Update preview image
806  self.image_updated.emit(image_path)
807 
808 
809  # Re-enable the interface
810  self.enable_interface.emit()
811 
812  # Check if NO FRAMES are detected
813  if not self.frame_detected:
814  # Show Error that no frames are detected. This is likely caused by
815  # the wrong command being executed... or an error in Blender.
816  self.blender_error_with_data.emit(_("No frame was found in the output from Blender"))
817 
818  # Done with render (i.e. close window)
819  elif not self.preview_mode:
820  # only close window if in 'render' mode
821  self.finished.emit()
822 
823  # Thread finished
824  log.info("Blender render thread finished")
825  if self.is_running == False:
826  # close window if thread was killed
827  self.closed.emit()
828 
829  # mark thread as finished
830  self.is_running = False
Background Worker Object (to run the Blender commands)
def Render(self, blend_file_path, target_script, preview_mode=False)
Worker&#39;s Render method which invokes the Blender rendering commands.
def inject_params(self, path, frame=None)
def currentChanged(self, selected, deselected)
def get_app()
Returns the current QApplication instance of OpenShot.
Definition: app.py:54
def disable_interface(self, cursor=True)
Disable all controls on interface.
def sliderPreview_valueChanged(self, new_value)
Get new value of preview slider, and start timer to Render frame.
A custom Blender QEvent, which can safely be sent from the Blender thread to the Qt thread (to commun...
A TreeView QWidget used on the animated title window.
def onUpdateProgress(self, current_frame, current_part, max_parts)
def Render(self, frame=None)
Render an images sequence of the current template using Blender 2.62+ and the Blender Python API...
def onUpdateImage(self, image_path)
def get_animation_details(self)
Build a dictionary of all animation settings and properties from XML.
def error_with_blender(self, version=None, command_output=None)
Show a friendly error message regarding the blender executable or version.
def dropdown_index_changed(self, widget, param, index)
def update_progress_bar(self, current_frame, current_part, max_parts)
def preview_timer_onTimeout(self)
Timer is ready to Render frame.
def text_value_changed(self, widget, param, value=None)
def enable_interface(self)
Disable all controls on interface.
def onBlenderVersionError(self, version)
def spinner_value_changed(self, param, value)
def generateUniqueFolder(self)
Generate a new, unique folder name to contain Blender frames.
def get_settings()
Get the current QApplication&#39;s settings instance.
Definition: settings.py:43
def __init__(self, id, data=None, args)
def get_project_params(self, is_preview=True)
Return a dictionary of project related settings, needed by the Blender python script.
def update_image(self, image_path)
def color_button_clicked(self, widget, param, index)
def init_slider_values(self)
Init the slider and preview frame label to the currently selected animation.