OpenShot Video Editor  2.0.0
title_editor.py
Go to the documentation of this file.
1 ##
2 #
3 # @file
4 # @brief This file loads the title editor dialog (i.e SVG creator)
5 # @author Jonathan Thomas <jonathan@openshot.org>
6 # @author Andy Finch <andy@openshot.org>
7 #
8 # @section LICENSE
9 #
10 # Copyright (c) 2008-2018 OpenShot Studios, LLC
11 # (http://www.openshotstudios.com). This file is part of
12 # OpenShot Video Editor (http://www.openshot.org), an open-source project
13 # dedicated to delivering high quality video editing and animation solutions
14 # to the world.
15 #
16 # OpenShot Video Editor is free software: you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation, either version 3 of the License, or
19 # (at your option) any later version.
20 #
21 # OpenShot Video Editor is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License for more details.
25 #
26 # You should have received a copy of the GNU General Public License
27 # along with OpenShot Library. If not, see <http://www.gnu.org/licenses/>.
28 #
29 
30 import sys
31 import os
32 import re
33 import shutil
34 import functools
35 import subprocess
36 import tempfile
37 from xml.dom import minidom
38 
39 from PyQt5.QtCore import *
40 from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem, QFont
41 from PyQt5.QtWidgets import *
42 from PyQt5 import uic, QtSvg, QtGui
43 import openshot
44 
45 from classes import info, ui_util, settings, qt_types, updates
46 from classes.logger import log
47 from classes.app import get_app
48 from classes.query import File
49 from classes.metrics import *
50 from windows.views.titles_listview import TitlesListView
51 
52 try:
53  import json
54 except ImportError:
55  import simplejson as json
56 
57 
58 ##
59 # Title Editor Dialog
60 class TitleEditor(QDialog):
61 
62  # Path to ui file
63  ui_path = os.path.join(info.PATH, 'windows', 'ui', 'title-editor.ui')
64 
65  def __init__(self, edit_file_path=None, duplicate=False):
66 
67  # Create dialog class
68  QDialog.__init__(self)
69 
70  self.app = get_app()
71  self.project = self.app.project
72  self.edit_file_path = edit_file_path
73  self.duplicate = duplicate
74 
75  # Get translation object
76  _ = self.app._tr
77 
78  # Load UI from designer
79  ui_util.load_ui(self, self.ui_path)
80 
81  # Init UI
82  ui_util.init_ui(self)
83 
84  # Track metrics
85  track_metric_screen("title-screen")
86 
87  # Initialize variables
88  self.template_name = ""
89  imp = minidom.getDOMImplementation()
90  self.xmldoc = imp.createDocument(None, "any", None)
91 
92  self.bg_color_code = QtGui.QColor(Qt.black)
93  self.font_color_code = QtGui.QColor(Qt.white)
94 
95  self.bg_style_string = ""
98 
99  self.font_weight = 'normal'
100  self.font_style = 'normal'
101 
102  self.new_title_text = ""
103  self.sub_title_text = ""
104  self.subTitle = False
105 
106  self.display_name = ""
107  self.font_family = "Bitstream Vera Sans"
108  self.tspan_node = None
109 
110  # Add titles list view
111  self.titlesTreeView = TitlesListView(self)
112  self.verticalLayout.addWidget(self.titlesTreeView)
113 
114  # Disable Save button on window load
115  self.buttonBox.button(self.buttonBox.Save).setEnabled(False)
116 
117  # If editing existing title svg file
118  if self.edit_file_path:
119  # Hide list of templates
120  self.widget.setVisible(False)
121 
122  # Create temp version of title
124 
125  # Add all widgets for editing
126  self.load_svg_template()
127 
128  # Display image (slight delay to allow screen to be shown first)
129  QTimer.singleShot(50, self.display_svg)
130 
131  def txtLine_changed(self, txtWidget):
132 
133  # Loop through child widgets (and remove them)
134  text_list = []
135  for child in self.settingsContainer.children():
136  if type(child) == QTextEdit and child.objectName() != "txtFileName":
137  text_list.append(child.toPlainText())
138 
139  # Update text values in the SVG
140  for i in range(0, self.text_fields):
141  if len(self.tspan_node[i].childNodes) > 0 and i <= (len(text_list) - 1):
142  new_text_node = self.xmldoc.createTextNode(text_list[i])
143  old_text_node = self.tspan_node[i].childNodes[0]
144  self.tspan_node[i].removeChild(old_text_node)
145  # add new text node
146  self.tspan_node[i].appendChild(new_text_node)
147 
148  # Something changed, so update temp SVG
149  self.writeToFile(self.xmldoc)
150 
151  # Display SVG again
152  self.display_svg()
153 
154  def display_svg(self):
155  # Create a temp file for this thumbnail image
156  new_file, tmp_filename = tempfile.mkstemp()
157  tmp_filename = "%s.png" % tmp_filename
158 
159  # Create a clip object and get the reader
160  clip = openshot.Clip(self.filename)
161  reader = clip.Reader()
162 
163  # Open reader
164  reader.Open()
165 
166  # Save thumbnail image and close readers
167  reader.GetFrame(1).Thumbnail(tmp_filename, self.graphicsView.width(), self.graphicsView.height(), "", "", "#000", False, "png", 100, 0.0)
168  reader.Close()
169  clip.Close()
170 
171  # Display temp image
172  scene = QGraphicsScene(self)
173  view = self.graphicsView
174  svg = QtGui.QPixmap(tmp_filename)
175  svg_scaled = svg.scaled(self.graphicsView.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
176  scene.addPixmap(svg_scaled)
177  view.setScene(scene)
178  view.show()
179 
180  def create_temp_title(self, template_path):
181 
182  # Set temp file path
183  self.filename = os.path.join(info.TITLE_PATH, "temp.svg")
184 
185  # Copy template to temp file
186  shutil.copy(template_path, self.filename)
187 
188  # return temp path
189  return self.filename
190 
191  ##
192  # Load an SVG title and init all textboxes and controls
193  def load_svg_template(self):
194 
195  # Get translation object
196  _ = get_app()._tr
197 
198  # parse the svg object
199  self.xmldoc = minidom.parse(self.filename)
200  # get the text elements
201  self.tspan_node = self.xmldoc.getElementsByTagName('tspan')
202  self.text_fields = len(self.tspan_node)
203 
204  # Loop through child widgets (and remove them)
205  for child in self.settingsContainer.children():
206  try:
207  self.settingsContainer.layout().removeWidget(child)
208  child.deleteLater()
209  except:
210  pass
211 
212  # Get text nodes and rect nodes
213  self.text_node = self.xmldoc.getElementsByTagName('text')
214  self.rect_node = self.xmldoc.getElementsByTagName('rect')
215 
216  # Create Label
217  label = QLabel()
218  label_line_text = _("File Name:")
219  label.setText(label_line_text)
220  label.setToolTip(label_line_text)
221 
222  # create text editor for file name
223  self.txtFileName = QTextEdit()
224  self.txtFileName.setObjectName("txtFileName")
225 
226  # If edit mode, set file name
227  if self.edit_file_path and not self.duplicate:
228  # Use existing name (and prevent editing name)
229  self.txtFileName.setText(os.path.split(self.edit_file_path)[1])
230  self.txtFileName.setEnabled(False)
231  else:
232  name = _("TitleFileName-%d")
233  offset = 0
234  if self.duplicate and self.edit_file_path:
235  # Re-use current name
236  name = os.path.split(self.edit_file_path)[1]
237  # Splits the filename into (base-part)(optional "-")(number)(.svg)
238  match = re.match(r"^(.*?)(-?)([0-9]*)(\.svg)?$", name)
239  # Make sure the new title ends with "-%d" by default
240  name = match.group(1) + "-%d"
241  if match.group(3):
242  # Filename already contained a number -> start counting from there
243  offset = int(match.group(3))
244  # -> only use "-" if it was there before
245  name = match.group(1) + match.group(2) + "%d"
246  # Find an unused file name
247  for i in range(1, 1000):
248  curname = name % (offset + i)
249  possible_path = os.path.join(info.ASSETS_PATH, "%s.svg" % curname)
250  if not os.path.exists(possible_path):
251  self.txtFileName.setText(curname)
252  break
253  self.txtFileName.setFixedHeight(28)
254  self.settingsContainer.layout().addRow(label, self.txtFileName)
255 
256  # Get text values
257  title_text = []
258  for i in range(0, self.text_fields):
259  if len(self.tspan_node[i].childNodes) > 0:
260  text = self.tspan_node[i].childNodes[0].data
261  title_text.append(text)
262 
263  # Create Label
264  label = QLabel()
265  label_line_text = _("Line %s:") % str(i + 1)
266  label.setText(label_line_text)
267  label.setToolTip(label_line_text)
268 
269  # create text editor for each text element in title
270  widget = QTextEdit()
271  widget.setText(_(text))
272  widget.setFixedHeight(28)
273  widget.textChanged.connect(functools.partial(self.txtLine_changed, widget))
274  self.settingsContainer.layout().addRow(label, widget)
275 
276 
277  # Add Font button
278  label = QLabel()
279  label.setText(_("Font:"))
280  label.setToolTip(_("Font:"))
281  self.btnFont = QPushButton()
282  self.btnFont.setText(_("Change Font"))
283  self.settingsContainer.layout().addRow(label, self.btnFont)
284  self.btnFont.clicked.connect(self.btnFont_clicked)
285 
286  # Add Text color button
287  label = QLabel()
288  label.setText(_("Text:"))
289  label.setToolTip(_("Text:"))
290  self.btnFontColor = QPushButton()
291  self.btnFontColor.setText(_("Text Color"))
292  self.settingsContainer.layout().addRow(label, self.btnFontColor)
293  self.btnFontColor.clicked.connect(self.btnFontColor_clicked)
294 
295  # Add Background color button
296  label = QLabel()
297  label.setText(_("Background:"))
298  label.setToolTip(_("Background:"))
299  self.btnBackgroundColor = QPushButton()
300  self.btnBackgroundColor.setText(_("Background Color"))
301  self.settingsContainer.layout().addRow(label, self.btnBackgroundColor)
302  self.btnBackgroundColor.clicked.connect(self.btnBackgroundColor_clicked)
303 
304  # Add Advanced Editor button
305  label = QLabel()
306  label.setText(_("Advanced:"))
307  label.setToolTip(_("Advanced:"))
308  self.btnAdvanced = QPushButton()
309  self.btnAdvanced.setText(_("Use Advanced Editor"))
310  self.settingsContainer.layout().addRow(label, self.btnAdvanced)
311  self.btnAdvanced.clicked.connect(self.btnAdvanced_clicked)
312 
313  # Update color buttons
316 
317  # Enable / Disable buttons based on # of text nodes
318  if len(title_text) >= 1:
319  self.btnFont.setEnabled(True)
320  self.btnFontColor.setEnabled(True)
321  self.btnBackgroundColor.setEnabled(True)
322  self.btnAdvanced.setEnabled(True)
323  else:
324  self.btnFont.setEnabled(False)
325  self.btnFontColor.setEnabled(False)
326 
327  # Enable Save button when a template is selected
328  self.buttonBox.button(self.buttonBox.Save).setEnabled(True)
329 
330  ##
331  # writes a new svg file containing the user edited data
332  def writeToFile(self, xmldoc):
333 
334  if not self.filename.endswith("svg"):
335  self.filename = self.filename + ".svg"
336  try:
337  file = open(self.filename.encode('UTF-8'), "wb") # wb needed for windows support
338  file.write(bytes(xmldoc.toxml(), 'UTF-8'))
339  file.close()
340  except IOError as inst:
341  log.error("Error writing SVG title")
342 
344  app = get_app()
345  _ = app._tr
346 
347  # Get color from user
348  col = QColorDialog.getColor(self.font_color_code, self, _("Select a Color"),
349  QColorDialog.DontUseNativeDialog | QColorDialog.ShowAlphaChannel)
350 
351  # Update SVG colors
352  if col.isValid():
353  self.set_font_color_elements(col.name(), col.alphaF())
355  self.font_color_code = col
356 
357  # Something changed, so update temp SVG
358  self.writeToFile(self.xmldoc)
359 
360  # Display SVG again
361  self.display_svg()
362 
364  app = get_app()
365  _ = app._tr
366 
367  # Get color from user
368  col = QColorDialog.getColor(self.bg_color_code, self, _("Select a Color"),
369  QColorDialog.DontUseNativeDialog | QColorDialog.ShowAlphaChannel)
370 
371  # Update SVG colors
372  if col.isValid():
373  self.set_bg_style(col.name(), col.alphaF())
375  self.bg_color_code = col
376 
377  # Something changed, so update temp SVG
378  self.writeToFile(self.xmldoc)
379 
380  # Display SVG again
381  self.display_svg()
382 
383  def btnFont_clicked(self):
384  app = get_app()
385  _ = app._tr
386 
387  # Get font from user
388  font, ok = QFontDialog.getFont(QFont(), caption=_("Change Font"))
389 
390  # Update SVG font
391  if ok:
392  fontinfo = QtGui.QFontInfo(font)
393  self.font_family = fontinfo.family()
394  self.font_style = fontinfo.styleName()
395  self.font_weight = fontinfo.weight()
396  self.set_font_style()
397 
398  # Something changed, so update temp SVG
399  self.writeToFile(self.xmldoc)
400 
401  # Display SVG again
402  self.display_svg()
403 
404  ##
405  # when passed a partial value, function will return the list index
406  def find_in_list(self, l, value):
407  for item in l:
408  if item.startswith(value):
409  return l.index(item)
410 
411  ##
412  # Updates the color shown on the font color button
414 
415  # Loop through each TEXT element
416  for node in self.text_node:
417 
418  # Get the value in the style attribute
419  s = node.attributes["style"].value
420 
421  # split the node so we can access each part
422  ar = s.split(";")
423  color = self.find_in_list(ar, "fill:")
424 
425  try:
426  # Parse the result
427  txt = ar[color]
428  color = txt[5:]
429  except:
430  # If the color was in an invalid format, try the next text element
431  continue
432 
433  opacity = self.find_in_list(ar, "opacity:")
434 
435  try:
436  # Parse the result
437  txt = ar[opacity]
438  opacity = float(txt[8:])
439  except:
440  pass
441 
442  # Default the font color to white if non-existing
443  if color == None:
444  color = "#FFFFFF"
445 
446  # Default the opacity to fully visible if non-existing
447  if opacity == None:
448  opacity = 1.0
449 
450  color = QtGui.QColor(color)
451 
452  # Compute perceptive luminance of background color
453  colrgb = color.getRgbF()
454  lum = (0.299 * colrgb[0] + 0.587 * colrgb[1] + 0.114 * colrgb[2])
455  if (lum < 0.5):
456  text_color = QtGui.QColor(Qt.white)
457  else:
458  text_color = QtGui.QColor(Qt.black)
459 
460  # Convert the opacity into the alpha value
461  alpha = int(opacity * 65535.0)
462 
463  # Set the colors of the button
464  self.btnFontColor.setStyleSheet(
465  "background-color: %s; opacity: %s; color: %s;"
466  % (color.name(), alpha, text_color.name()))
467  self.font_color_code = color
468 
469  ##
470  # Updates the color shown on the background color button
472 
473  if self.rect_node:
474 
475  # All backgrounds should be the first (index 0) rect tag in the svg
476  s = self.rect_node[0].attributes["style"].value
477 
478  # split the node so we can access each part
479  ar = s.split(";")
480 
481  color = self.find_in_list(ar, "fill:")
482 
483  try:
484  # Parse the result
485  txt = ar[color]
486  color = txt[5:]
487  except ValueError:
488  pass
489 
490  opacity = self.find_in_list(ar, "opacity:")
491 
492  try:
493  # Parse the result
494  txt = ar[opacity]
495  opacity = float(txt[8:])
496  except ValueError:
497  pass
498  except TypeError:
499  pass
500 
501  # Default the background color to black if non-existing
502  if color == None:
503  color = "#000000"
504 
505  # Default opacity to fully visible if non-existing
506  if opacity == None:
507  opacity = 1.0
508 
509  color = QtGui.QColor(color)
510 
511  # Compute perceptive luminance of background color
512  colrgb = color.getRgbF()
513  lum = (0.299 * colrgb[0] + 0.587 * colrgb[1] + 0.114 * colrgb[2])
514  if (lum < 0.5):
515  text_color = QtGui.QColor(Qt.white)
516  else:
517  text_color = QtGui.QColor(Qt.black)
518 
519  # Convert the opacity into the alpha value
520  alpha = int(opacity * 65535.0)
521 
522  # Set the colors of the button
523  self.btnBackgroundColor.setStyleSheet(
524  "background-color: %s; opacity: %s; color: %s;"
525  % (color.name(), alpha, text_color.name()))
526  self.bg_color_code = color
527 
528  ##
529  # sets the font properties
530  def set_font_style(self):
531 
532  # Loop through each TEXT element
533  for text_child in self.text_node:
534  # set the style elements for the main text node
535  s = text_child.attributes["style"].value
536  # split the text node so we can access each part
537  ar = s.split(";")
538  # we need to find each element that we are changing, shouldn't assume
539  # they are in the same position in any given template.
540 
541  # ignoring font-weight, as not sure what it represents in Qt.
542  fs = self.find_in_list(ar, "font-style:")
543  ff = self.find_in_list(ar, "font-family:")
544  if fs:
545  ar[fs] = "font-style:" + self.font_style
546  if ff:
547  ar[ff] = "font-family:" + self.font_family
548  # rejoin the modified parts
549  t = ";"
550  self.title_style_string = t.join(ar)
551 
552  # set the text node
553  text_child.setAttribute("style", self.title_style_string)
554 
555  # Loop through each TSPAN
556  for tspan_child in self.tspan_node:
557  # set the style elements for the main text node
558  s = tspan_child.attributes["style"].value
559  # split the text node so we can access each part
560  ar = s.split(";")
561  # we need to find each element that we are changing, shouldn't assume
562  # they are in the same position in any given template.
563 
564  # ignoring font-weight, as not sure what it represents in Qt.
565  fs = self.find_in_list(ar, "font-style:")
566  ff = self.find_in_list(ar, "font-family:")
567  if fs:
568  ar[fs] = "font-style:" + self.font_style
569  if ff:
570  ar[ff] = "font-family:" + self.font_family
571  # rejoin the modified parts
572  t = ";"
573  self.title_style_string = t.join(ar)
574 
575  # set the text node
576  tspan_child.setAttribute("style", self.title_style_string)
577 
578  ##
579  # sets the background color
580  def set_bg_style(self, color, alpha):
581 
582  if self.rect_node:
583  # split the node so we can access each part
584  s = self.rect_node[0].attributes["style"].value
585  ar = s.split(";")
586  fill = self.find_in_list(ar, "fill:")
587  if fill == None:
588  ar.append("fill:" + color)
589  else:
590  ar[fill] = "fill:" + color
591 
592  opacity = self.find_in_list(ar, "opacity:")
593  if opacity == None:
594  ar.append("opacity:" + str(alpha))
595  else:
596  ar[opacity] = "opacity:" + str(alpha)
597 
598  # rejoin the modified parts
599  t = ";"
600  self.bg_style_string = t.join(ar)
601  # set the node in the xml doc
602  self.rect_node[0].setAttribute("style", self.bg_style_string)
603 
604  def set_font_color_elements(self, color, alpha):
605 
606  # Loop through each TEXT element
607  for text_child in self.text_node:
608 
609  # SET TEXT PROPERTIES
610  s = text_child.attributes["style"].value
611  # split the text node so we can access each part
612  ar = s.split(";")
613  fill = self.find_in_list(ar, "fill:")
614  if fill == None:
615  ar.append("fill:" + color)
616  else:
617  ar[fill] = "fill:" + color
618 
619  opacity = self.find_in_list(ar, "opacity:")
620  if opacity == None:
621  ar.append("opacity:" + str(alpha))
622  else:
623  ar[opacity] = "opacity:" + str(alpha)
624 
625  t = ";"
626  text_child.setAttribute("style", t.join(ar))
627 
628 
629  # Loop through each TSPAN
630  for tspan_child in self.tspan_node:
631 
632  # SET TSPAN PROPERTIES
633  s = tspan_child.attributes["style"].value
634  # split the text node so we can access each part
635  ar = s.split(";")
636  fill = self.find_in_list(ar, "fill:")
637  if fill == None:
638  ar.append("fill:" + color)
639  else:
640  ar[fill] = "fill:" + color
641  t = ";"
642  tspan_child.setAttribute("style", t.join(ar))
643 
644  def accept(self):
645  app = get_app()
646  _ = app._tr
647 
648  # If editing file, just update the existing file
649  if self.edit_file_path and not self.duplicate:
650  # Update filename
651  self.filename = self.edit_file_path
652 
653  # Overwrite title svg file
654  self.writeToFile(self.xmldoc)
655 
656  else:
657  # Create new title (with unique name)
658  file_name = "%s.svg" % self.txtFileName.toPlainText().strip()
659  file_path = os.path.join(info.ASSETS_PATH, file_name)
660 
661  if self.txtFileName.toPlainText().strip():
662  # Do we have unsaved changes?
663  if os.path.exists(file_path) and not self.edit_file_path:
664  ret = QMessageBox.question(self, _("Title Editor"), _("%s already exists.\nDo you want to replace it?") % file_name,
665  QMessageBox.No | QMessageBox.Yes)
666  if ret == QMessageBox.No:
667  # Do nothing
668  return
669 
670  # Update filename
671  self.filename = file_path
672 
673  # Save title
674  self.writeToFile(self.xmldoc)
675 
676  # Add file to project
677  self.add_file(self.filename)
678 
679  # Close window
680  super(TitleEditor, self).accept()
681 
682  def add_file(self, filepath):
683  path, filename = os.path.split(filepath)
684 
685  # Add file into project
686  app = get_app()
687  _ = get_app()._tr
688 
689  # Check for this path in our existing project data
690  file = File.get(path=filepath)
691 
692  # If this file is already found, exit
693  if file:
694  return
695 
696  # Load filepath in libopenshot clip object (which will try multiple readers to open it)
697  clip = openshot.Clip(filepath)
698 
699  # Get the JSON for the clip's internal reader
700  try:
701  reader = clip.Reader()
702  file_data = json.loads(reader.Json())
703 
704  # Set media type
705  file_data["media_type"] = "image"
706 
707  # Save new file to the project data
708  file = File()
709  file.data = file_data
710  file.save()
711  return True
712 
713  except:
714  # Handle exception
715  msg = QMessageBox()
716  msg.setText(_("{} is not a valid video, audio, or image file.".format(filename)))
717  msg.exec_()
718  return False
719 
721  _ = self.app._tr
722  # use an external editor to edit the image
723  try:
724  # Get settings
726 
727  # get the title editor executable path
728  prog = s.get("title_editor")
729 
730  # launch advanced title editor
731  # debug info
732  log.info("Advanced title editor command: {} {} ".format(prog, self.filename))
733 
734  p = subprocess.Popen([prog, self.filename])
735 
736  # wait for process to finish (so we can update the preview)
737  p.communicate()
738 
739  # update image preview
740  self.load_svg_template()
741  self.display_svg()
742 
743  except OSError:
744  msg = QMessageBox()
745  msg.setText(_("Please install {} to use this function").format(prog.capitalize()))
746  msg.exec_()
Title Editor Dialog.
Definition: title_editor.py:60
def set_bg_style
sets the background color
def track_metric_screen
Track a GUI screen being shown.
Definition: metrics.py:96
def get_app
Returns the current QApplication instance of OpenShot.
Definition: app.py:55
def set_font_style
sets the font properties
def load_svg_template
Load an SVG title and init all textboxes and controls.
def find_in_list
when passed a partial value, function will return the list index
def update_background_color_button
Updates the color shown on the background color button.
def load_ui
Load a Qt *.ui file, and also load an XML parsed version.
Definition: ui_util.py:66
def update_font_color_button
Updates the color shown on the font color button.
def init_ui
Initialize all child widgets and action of a window or dialog.
Definition: ui_util.py:215
def writeToFile
writes a new svg file containing the user edited data
def get_settings
Get the current QApplication's settings instance.
Definition: settings.py:44