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