Source code for hiero.core.FnExporterBase

# Copyright (c) 2011 The Foundry Visionmongers Ltd.  All Rights Reserved.

import collections
import os
import re
import math

import time
import datetime
import getpass
import itertools

from hiero.core import ITask
from hiero.core import ITaskPreset
from hiero.core import defaultFrameRates
from hiero.core import ItemWrapper
from hiero.core import ResolveTable
from hiero.core import TrackItem
from hiero.core import MediaSource
from hiero.core import Clip
from hiero.core import Sequence
from hiero.core import ApplicationSettings
from hiero.core import isVideoFileExtension
from hiero.core import Keys
from hiero.core import ResolveTable
from hiero.core import log
from hiero.core import VideoTrack
from hiero.core import AudioTrack
from hiero.core import Transition
from hiero.core import remapPath
from hiero.core import util
from . FnCompSourceInfo import CompSourceInfo
from .FnFloatRange import *
from collections import OrderedDict
import types
import sys


#Given a type, return a string representation stripped of unnecessary characters
def classBasename( objecttype ):
  if isinstance(objecttype, str):
    return objecttype

  #<class 'hiero.exporters.FnNukeShotExporter.NukeShotPreset'>
  #to
  #hiero.exporters.FnNukeShotExporter.NukeShotPreset
  module = objecttype.__module__
  if module is None or module == str.__class__.__module__:
    return objecttype.__name__
  else:
    return module + '.' + objecttype.__name__

[docs]class TaskData(dict): """TaskData is used as a seed for creating classes, wrapping up all of the parameters and making it simpler to add new ones""" kPreset = "preset" kItem = "item" kExportRoot = "exportRoot" kShotPath = "shotPath" kVersion = "version" kExportTemplate = "exportTemplate" kResolver = "resolver" kCutHandles = "cutHandles" kRetime = "retime" kStartFrameSource = "startFrameSource" # Parameter indicating where the start frame should be taken from. Current possible values are Source, Sequence and Custom kStartFrame = "startFrame" kProject = "project" kSubmission = "submission" kSkipOffline = "skipOffline" kPresetId = "presetId" kShotNameIndex = "shotNameIndex" kMediaToSkip = "mediaToSkip" def __init__( self, preset, item, exportRoot, shotPath, version, exportTemplate, project, cutHandles=None, resolver=None, retime=False, startFrame=None, startFrameSource=None, submission=None, skipOffline=True, presetId=None, shotNameIndex='', mediaToSkip=[]): dict.__init__(self) self[TaskData.kPreset] = preset self[TaskData.kItem] = item self[TaskData.kExportRoot] = exportRoot self[TaskData.kShotPath] = shotPath self[TaskData.kVersion] = version self[TaskData.kExportTemplate] = exportTemplate self[TaskData.kCutHandles] = cutHandles self[TaskData.kRetime] = retime self[TaskData.kStartFrame] = startFrame self[TaskData.kStartFrameSource] = startFrameSource self[TaskData.kProject] = project self[TaskData.kSkipOffline] = skipOffline self[TaskData.kPresetId] = presetId self[TaskData.kShotNameIndex] = shotNameIndex self[TaskData.kMediaToSkip] = mediaToSkip if submission is not None: self[TaskData.kSubmission] = submission if resolver is None: if preset: self[TaskData.kResolver] = preset.createResolver() else: self[TaskData.kResolver] = None else: self[TaskData.kResolver] = resolver.duplicate() if preset: self[TaskData.kResolver].merge(preset.createResolver())
class TaskCallbacks(object): """ This class manages callback functions that can be called when a Task goes into a particular state. """ _callbacks = collections.defaultdict(list) # callback types... onTaskStart = "onTaskStart" onTaskFinish = "onTaskFinish" @classmethod def addCallback(cls, callbackType, callbackFn): """ Add the given callback function and callbackType. The callback will be executed when TaskCallbacks.call is called with the same callbackType. """ cls._callbacks[callbackType].append(callbackFn) @classmethod def call(cls, callbackType, task): """ Call all the callback functions for the given callbackType passing in task as the only argument. """ for callbackFn in cls._callbacks[callbackType]: callbackFn(task)
[docs]class TaskBase(ITask): """TaskBase is the base class from which all Tasks must derrive.""" def __init__ ( self, initDictionary ): """__init__(self, initDictionary) Initialise TaskBase Class @param initDictionary: a TaskData dictionary which seeds the task with all initialization data """ ITask.__init__(self) preset = initDictionary[TaskData.kPreset] self._presetId = initDictionary[TaskData.kPresetId] item = initDictionary[TaskData.kItem] exportRoot = initDictionary[TaskData.kExportRoot] shotPath = initDictionary[TaskData.kShotPath] version = initDictionary[TaskData.kVersion] exportTemplate = initDictionary[TaskData.kExportTemplate] # The number of frames of handles to be included when exporting a track # item. If this is None, it means the full clip length should be exported. self._cutHandles = initDictionary[TaskData.kCutHandles] self._resolver = initDictionary[TaskData.kResolver] self._retime = initDictionary[TaskData.kRetime] self._startFrame = initDictionary[TaskData.kStartFrame] self._startFrameSource = initDictionary[TaskData.kStartFrameSource] self._shotNameIndex = initDictionary[TaskData.kShotNameIndex] self._skipOffline = True if TaskData.kSkipOffline in initDictionary: self._skipOffline = initDictionary[TaskData.kSkipOffline] self._mediaToSkip = initDictionary[TaskData.kMediaToSkip] self._submission = None if TaskData.kSubmission in initDictionary: self._submission = initDictionary[TaskData.kSubmission] self._projectName, self._projectRoot, self._projectSettings = "", "", None self._project = initDictionary[TaskData.kProject] if self._project is not None: self._projectName = self._project.name() self._projectRoot = self._project.exportRootDirectory() try: self._projectSettings = self._project.extractSettings() except Exception as e: # may throw an exception if project setting is invalid self.setError(str(e)) # Grab timestamp at time of creation. Primarily used for resolving date tokens. self._timeStamp = datetime.datetime.now() self._sequence = None self._track = None self._trackitem = None self._clip = None self._source = None self._fileinfo = None self._exportTemplate = exportTemplate self._item = item self._filename = None self._filebase = None self._fileext = None if isinstance(version, str): self._versionString = version else: self._versionString = "v%02d" % version if isinstance(item, (TrackItem, Clip)): # If these fail, the object is badly formed -- it has no internal C++ object. assert item, "Null Item." if isinstance(item, TrackItem): self._clip = item.source() self._track = item.parent() assert self._track, "Null Parent Track" self._sequence = self._track.parent() assert self._sequence, "Null Sequence" else: self._clip = item assert self._clip, "Null clip." assert isinstance(self._clip, Clip), "Track item does not contain a source clip." self._source = self._clip.mediaSource() assert self._source, "Null source." # Get the filepath info from the clip. Currently we only allow one but when more # are permitted in the future (eg stereo clips) this will need updating. self._fileinfo = self._source.fileinfos()[0] filename = self._fileinfo.filename() if filename is not None and len(filename) > 0: self._filename = os.path.basename(filename) self._filebase, self._fileext = os.path.splitext(self._filename) # Remove fragment index from r3d files B007_C006_0321VN_002.R3D (_002 is usually hidden from the user) if self._fileext is not None and self._fileext.lower() == ".r3d": self._filebase = self._filebase[:16] self._filename = self._filebase + self._fileext elif isinstance(item, Sequence): self._sequence = item assert self._sequence, "Null Sequence" self._item = item self._preset = preset self._exportRoot = exportRoot.replace("\\", "/") self._shotPath = shotPath.replace("\\", "/") self._version = version self._exportPath = os.path.join(self._exportRoot, self._shotPath) desc = self.ident().rsplit('.', 1)[1] self.setFormatDescription(desc) self.setDestinationDescription(os.path.dirname(self.resolvedExportPath())) self._finished = False
[docs] def timeStampString(self, localtime): """timeStampString(localtime) Convert a tuple or struct_time representing a time as returned by gmtime() or localtime() to a string formated YEAR/MONTH/DAY TIME.""" return time.strftime("%Y/%m/%d %X", localtime)
[docs] def setError(self, desc): """setError(self, desc) Call to set the state of this task to error, with a description of the error. If the task is synchronous, raise exception""" ITask.setError(self, desc) if self.synchronous(): raise Exception(desc)
[docs] def validate(self): """ Check that the task is in a state that allows it to be executed. Should raise an exception if there is an error. The default implementation checks if the task can be used with a multi-view project. """ multiViewProject = len(self._project.views()) > 1 if multiViewProject and not self.views(): raise RuntimeError("{} does not support multiple views".format(self.ident().split('.')[-1]))
[docs] def updateItem (self, originalItem, localtime): """updateItem - This is called by the processor prior to taskStart, crucially on the main thread.\n This gives the task an opportunity to modify the original item on the main thread, rather than the clone.""" pass
[docs] def timeStamp(self): """timeStamp(self) Returns the datetime object from time of task creation""" return self._timeStamp
[docs] def fileName(self): """filename(self) Returns the source items filename if applicable""" return self._filename
[docs] def fileext(self): """fileext(self) Returns the source items file extention if applicable""" return (self._fileext[1:] if self._fileext else "")
[docs] def filebase(self): """filebase(self) Returns the source items file path less extension if applicable""" return self._filebase
[docs] def filehead(self): """filehead(self) Returns the source filename excluding image sequence frame padding and extension, if applicable""" if self._source: return mediaSourceExportFileHead(self._source) return self._filebase
[docs] def filepath(self): """filepath(self) Returns the source file path, if applicable""" if self._source: return os.path.dirname(self._source.firstpath()) return ""
[docs] def filepadding(self): """filepadding(self) Returns the padding used in source file if an image sequence, empty string otherwise""" if self._source: padding = self._source.filenamePadding() return "%%0%id" % padding return ""
[docs] def shotName(self): """shotName(self) Returns the Tasks track item name""" return self._item.name()
[docs] def clipName(self): """clipName(self) Returns the name of the clip in the bin""" return self._clip.name()
[docs] def trackName(self): """trackName(self) Returns the name of the parent track""" return self._track.name()
[docs] def versionString(self): """versionString(self) Returns the version string used to resolve the {version} token""" return self._versionString
[docs] def sequenceName(self): """sequenceName(self) Returns the name of the sequence or parent sequence (if exporting a track item)""" if self._sequence: return self._sequence.name() else: return ""
[docs] def shotNameIndex(self): """ shotNameIndex(self) Returns the index string for the shot, if there are multiple shots with the same name on the sequence. """ return self._shotNameIndex
[docs] def name(self): return str(type(self))
[docs] def projectName(self): """projectName(self) Returns the name of the project, used for resolving the {project} token)""" return str( self._projectName )
[docs] def projectRoot(self): """projectRoot(self) Returns the project root export path, used for resolving the {projectroot} token""" return self._projectRoot
[docs] def editId(self): """ editId(self) Returns a str containing the id of this edit. See hiero.core.TrackItem.eventNumber(). """ if (self._item != None) and isinstance(self._item, TrackItem): eventId = self._item.eventNumber() # When the track items are cloned, a tag is added which tracks the parent object, and contains the eventid # Take the event id from the tag because the cloned parent sequence may have been cropped changing the eventids eventTag = [ tag.metadata().value('tag.event') for tag in self._item.tags() if tag.metadata().hasKey('tag.event')] # If a tag has been found containing the event metadata return that value if eventTag: eventId = int(eventTag[0]) return str(eventId).rjust( self._editIdPadding(), '0' ) else: return "UnknownEditId"
def _editIdPadding(self): """ Get the padding for editId strings, based on the total number of track items in the sequence """ if not self._sequence: return 0 totalTrackItems = 0 for track in itertools.chain( self._sequence.videoTracks(), self._sequence.audioTracks() ): totalTrackItems = totalTrackItems + track.numItems() # Use at least 3 digits of padding, or more if there are more than 999 track items return max( 3, len(str(totalTrackItems)) )
[docs] def edlEditId(self): """ edlEditId(self) Returns the id taken from the EDL used to create this edit, if there was one. """ if (self._item != None) and isinstance(self._item, TrackItem): metadata = self._item.metadata() key = "foundry.edl.editNumber" if metadata.hasKey(key): return metadata.value(key) return "UnknownEditId"
[docs] def ident(self): """ident(self) Returns a string used for identifying the type of export task""" return classBasename(type(self))
[docs] def addToQueue(self): """addToQueue(self) Called by the processor in order to add the Task to the ExportQueue If derrived classes impliment this function, this base function must be called. Populates name, description and destination fields in the export queue""" ITask.addToQueue(self) # Set error state if the task is not able to run if not self.hasValidItem(): itemTypeString = "sequence" if (self._preset.supportedItems() == TaskPresetBase.kSequence) else "clip" self.setError("Task cannot be run, only valid for a %s" % itemTypeString)
[docs] def printState(self): """Print summary of the task parameters""" print("TaskBase -- task state:") print(" - preset:", self._preset) print(" - sequence:", self._sequence) if self._trackitem: print(" - trackitem:", self._trackitem) print(" - clip:", self._clip) print(" - source:", self._source) print(" - shotPath:", self._shotPath) print(" - exportRoot:", self._exportRoot) print(" - version:", self._version) print(" - exportPath:", self._exportPath) print(" - resolve table: ", str(self._resolver._resolvers))
[docs] def resolvePath(self, path): """Replace any recognized tokens in path with their current value.""" # Replace Windows path separators before token resolve path = path.replace("\\", "/") try: # Resolve token in path path = self._resolver.resolve(self, path, isPath=True) except RuntimeError as error: self.setError(str(error)) # Strip padding out of single file types if isVideoFileExtension(os.path.splitext(path)[1].lower()): path = re.sub(r'.[#]+', '', path) path = re.sub(r'.%[\d]+d', '', path) # Normalise path to use / for separators path = path.replace("\\", "/") # Strip trailing spaces on directory names. This causes problems on Windows # because it will not let you create a directory ending with a space, so if you do # e.g. mkdir("adirectory ") the space will be silently removed. path = path.replace(" /", "/") return path
[docs] def resolvedExportPath(self): """resolvedExportPath() returns the output path with and tokens resolved""" return self.resolvePath(self._exportPath)
def _outputHandles ( self, ignoreRetimes ): """ Internal _outputHandles() method. Should be reimplemented by sub-classes rather than outputHandles(). """ startH, endH = self.inputRange(ignoreHandles=False, ignoreRetimes=ignoreRetimes, clampToSource=False) start, end = self.inputRange(ignoreHandles=True, ignoreRetimes=ignoreRetimes, clampToSource=False) return int(start - startH), int(endH - end)
[docs] def outputHandles ( self, ignoreRetimes = False): """outputHandles( ignoreRetimes = False ) Return a tuple of the in/out handles generated by this task. Handles may be cropped such as to prevent negative frame indexes. Note that both handles are positive, i.e. if 12 frames of handles are specified, this will return (12, 12) Sub-classes should reimplement _outputHandles() rather than this method. @return: (in_handle, out_handle) tuple """ startHandle, endHandle = self._outputHandles(ignoreRetimes) if startHandle < 0 or endHandle < 0: raise RuntimeError("TaskBase.outputHandles error, values must not be negative %s %s" % (startHandle, endHandle)) return startHandle, endHandle
[docs] def availableOutputHandles(self): """ Get the available output handles, based on self._cutHandles. If outputting to sequence time, the start handle is clamped to prevent going into negative frames. """ if self.outputSequenceTime() and isinstance(self._item, TrackItem): return min(self._cutHandles, self._item.timelineIn()), self._cutHandles else: return self._cutHandles, self._cutHandles
[docs] def inputRange(self, ignoreHandles=False, ignoreRetimes=False, clampToSource=True): """inputRange() Returns the input frame range (as a tuple) for this task if applicable @param: ignoreHandles - If True calculate Input Range excluding export handles @param: ignoreRetimes - If True calculate Input Range without taking retimes into account @param: clampToSource - If True the input range will be clamped to the available media range""" log.debug( ">>> inputRange()" ) start, end = 0, 0 if isinstance(self._item, (TrackItem, Clip)): if self._cutHandles is None: # Exporting the whole clip or soft trims range. start = 0 end = self._clip.duration()-1 if self._clip.softTrimsEnabled(): start = self._clip.softTrimsInTime() end = self._clip.softTrimsOutTime() else: # Exporting only the amount cut in handles. ti = self._item log.debug( " ti.sourceIn() =" + str(ti.sourceIn()) ) log.debug( " ti.sourceOut() =" + str(ti.sourceOut()) ) log.debug( " self._cutHandles ="+ str(self._cutHandles) ) # Ensure _start <= _end (for negative retimes, sourceIn > sourceOut) sourceInOut = (ti.sourceIn(), ti.sourceOut()) start = min(sourceInOut) end = max(sourceInOut) inHandle, outHandle = 0, 0 # Don't include handles if the clip is a freeze frame isFreezeFrame = (ignoreRetimes == False and self._retime and ti.playbackSpeed() == 0.0) if ignoreHandles is False and not isFreezeFrame: inHandle, outHandle = self.availableOutputHandles() # Add transition Handles inTransition, outTransition = ti.inTransition(), ti.outTransition() inTransitionHandle, outTransitionHandle = 0, 0 if outTransition is not None and not outTransition.isNull(): if outTransition.alignment() == Transition.kDissolve: # Calculate the delta required to move the end of the clip to cover the disolve transition outTransitionHandle = (outTransition.timelineOut() - ti.timelineOut()) outHandle += outTransitionHandle log.debug(" outTransitionHandle = " + str(outTransitionHandle)) if inTransition is not None and not inTransition.isNull(): if inTransition.alignment() == Transition.kDissolve: # Calculate the delta required to move the begining of the clip to cover the disolve transition inTransitionHandle = (ti.timelineIn() - inTransition.timelineIn()) inHandle += inTransitionHandle log.debug( " inTransitionHandle = " + str(inTransitionHandle) ) log.debug(" ignoreRetimes = %s, self._retime = %s" % (str(ignoreRetimes), str(self._retime))) # Compensate for retimes in handle length if not ignoreRetimes and self._retime is True: retimeRate = ti.playbackSpeed() inHandle = math.ceil(inHandle * retimeRate) outHandle = math.ceil(outHandle * retimeRate) # Apply handles to start/end frame start = start - inHandle end = end + outHandle log.debug( " relative start =" + str(start) ) log.debug( " relative end =" + str(end) ) firstFrameNumber = self._clip.sourceIn() lastFrameNumber = self._clip.sourceOut() log.debug( " firstFrameNumber =" + str(firstFrameNumber) ) log.debug( " lastFrameNumber =" + str(lastFrameNumber) ) # Offset start and end by offset clip wrapper offset and starting # frame number to map into the file frame range. start = start + firstFrameNumber end = end + firstFrameNumber log.debug( " file start =" + str(start) ) log.debug( " file end =" + str(end) ) if clampToSource: # Trim back to the available file range. start = max(start, firstFrameNumber) end = min(end, lastFrameNumber) # If not clamping to source, we at least need to make sure the start frame is not negative else: start = max(start, 0) log.debug( " export start =" + str(start) ) log.debug( " export end =" + str(end) ) return (start, end)
[docs] def outputSequenceTime(self): """ Test if the output frame range should be in sequence time rather than source. This only applies when a TrackItem is being exported. NOTE: This option has been disabled for the time being. The code is left in place in case we want to re-enable it, but it is not available to users. """ return False
#return (self._startFrameSource == "Sequence")
[docs] def outputRange(self, ignoreHandles=False, ignoreRetimes=False, clampToSource=True): """outputRange() Returns the output file range (as tuple) for this task, if applicable. This default implementation works if the task was initialised with a Clip or TrackItem""" start = 0 end = 0 if isinstance(self._item, (TrackItem, Clip)): # Get input frame range start, end = self.inputRange(ignoreHandles=ignoreHandles, ignoreRetimes=ignoreRetimes, clampToSource=clampToSource) start = int(math.floor(start)) end = int(math.ceil(end)) # Offset by custom start time if self._startFrame is not None: end = self._startFrame + (end - start) start = self._startFrame log.debug( ">>> outputRange()" ) log.debug( " start =" + str(start) ) log.debug( " end =" + str(end) ) return (start, end)
[docs] def preSequence(self): """preSequence() This function serves as hook for custom scripts to add functionality before a task starts exporting anything with the sequence""" pass
[docs] def postSequence(self): """preSequence() This function serves as hook for custom scripts to add functionality on completion of exporting the contents of the sequence""" pass
[docs] def startTask(self): """startTask() Called when task reaches head of the export queue and begins execution""" TaskCallbacks.call(TaskCallbacks.onTaskStart, self) self.preSequence() # Build resolved path self._makePath()
[docs] def views(self): """ Get the view names used by the task. Tasks which support exporting from multi-view projects should reimplement this to return a non-empty list. """ return []
def _makePath(self): """_makePath() Resolve export path and make directories as neccessary.""" def makeAndCheckDir(dirPath): try: # If the destination path doesnt already exist, create it. util.filesystem.makeDirs(dirPath) # Ensure write access to this path if not util.filesystem.access(dirPath, os.W_OK | os.X_OK): self.setError("Insufficient permissions to write to directory '%s'" % dirPath) # Set error in case of exceptions except Exception as e: self.setError("Failed to create directory '%s'\n%s" % (dirPath, str(e))) # check export root exists, if not create. If the path has multi-view %v placeholders, # create a directory for each view dirPath = os.path.dirname(self.resolvedExportPath()) if util.isMultiViewPath(dirPath): if not self.views(): self.setError("No views set for multiview path {}".format(dirPath)) for view in self.views(): makeAndCheckDir(util.formatMultiViewPath(dirPath, view)) if self.error(): break else: makeAndCheckDir(dirPath)
[docs] def taskStep(self): """taskStep() Called every frame until task completes. Return True value to indicate task requires more steps. Return False value to indicate synchronous processing of the task is complete. The task may continue to run in the background. """ return False
[docs] def progress(self): """progress() Returns a float value 0..1 to indicate progress of task. The task is considered complete once the progress is reported as 1. """ if self._finished: return 1.0 else: return 0.0
[docs] def finishTask(self): """finishTask() Called once Task has signaled completion. Sub-classes should call this base implementation. """ TaskCallbacks.call(TaskCallbacks.onTaskFinish, self) self._finished = True self.postSequence() # Release cloned items self._item = None self._trackitem = None self._track = None self._sequence = None self._clip = None self._source = None self._fileinfo = None
def _sequenceHasAudio (self, sequence): for track in sequence.audioTracks(): for trackItem in track: if trackItem.source(): return True return False
[docs] def hasValidItem(self): """Get if the task is able to run on the item it was initialised with.""" supportedTypes = self._preset.supportedItems() supported = False if TaskPresetBase.kSequence & supportedTypes: supported |= isinstance(self._item, Sequence) if TaskPresetBase.kTrackItem & supportedTypes: supported |= isinstance(self._item, TrackItem) and self._item.mediaType() == TrackItem.kVideo if TaskPresetBase.kAudioTrackItem & supportedTypes: supported |= isinstance(self._item, TrackItem) and self._item.mediaType() == TrackItem.kAudio if TaskPresetBase.kClip & supportedTypes: supported |= isinstance(self._item, Clip) return supported
[docs] def supportedType(self, item): """Interface for defining what type of items a Task Supports. Return True to indicate item is of supported type""" # Derived classes must override to specify what types they support. # Typically this is Sequence or TrackItem. if type(self) is TaskBase: return True return False
[docs] def isExportingItem(self, item): """ Check if this task is already including an item in its export. Used for preventing duplicates when collating shots into a single script. """ return False
[docs] def deleteTemporaryFile(self, filePath): """ Delete a file which is an artifact of the export, but should be removed after it's finished. Returns whether the file was successfully deleted.""" # The reason for this behaviour is that we have occasional problems with tests failing on Windows # when removing temporary log files after a transcode. We don't want the export to be considered # in error when this happens, but it should be logged. try: os.unlink(filePath) return True except Exception as e: log.info("Deleting temporary file failed: %s" % str(e)) return False
[docs]class TaskGroup(ITask): """ TaskGroup is a Task which maintains a list of child Tasks. """ def __init__(self): ITask.__init__(self) self._children = []
[docs] def addChild(self, child): """ Add a child to the list. """ self._children.append(child)
[docs] def children(self): """ Get the TaskGroup's children. """ return self._children
[docs] def getLeafTasks(self): """ Get a list of all leaf tasks recursively, i.e. those with no child tasks. """ leafTasks = [] for child in self._children: if issubclass(type(child), TaskGroup): leafTasks.extend(child.getLeafTasks()) else: leafTasks.append(child) # Leaf return leafTasks
[docs] def addToQueue(self): try: ITask.addToQueue(self) except NotImplementedError: pass
[docs] def progress(self): """ Get the group progress. Returns a value based on the progress of child tasks. """ progress = 0.0 count = len(self._children) for child in self._children: progress += (child.progress() / count) return progress
[docs]class TaskPresetBase(ITaskPreset): """TaskPreset is the base class from which all Task Presets must derrive The purpose of a Task Preset is to store and data which must be serialized to file and shared between the Task and TaskUI user interface component""" def __init__ (self, parentType, presetName): """Initialise Exporter Preset Base Class @param parentType: Task type to which this preset object corresponds @param presetName: Name of preset""" ITaskPreset.__init__(self) self._name = presetName self._properties = {} self._nonPersistentProperties = {} self._parentType = parentType self._savePath = "" self._delete = False self._readOnly = False self._project = None self._skipOffline = True # Lists of MediaSources with comps to either render or skip self._compsToRender = [] self._compsToSkip = []
[docs] def initialiseCallbacks(self, exportStructure): """ When parent ExportStructure is opened in the ui, initialise is called for each preset. Register any callbacks here. """ pass
def __eq__( self, other ): """Implement equal operator. This will compare the TaskPreset name and it's properties. This method will ignore difference between lists an tuples, since the same TaskPreset can be copied and the only change existing is a list instead of a tuple.""" if not isinstance( other , TaskPresetBase ): return False if self.name() != other.name(): return False selfPropsKeys = sorted( self.properties().keys() ) otherPropsKeys = sorted( other.properties().keys() ) if selfPropsKeys != otherPropsKeys: return False exportTemplateKey = 'exportTemplate' if exportTemplateKey in selfPropsKeys: selfPropsKeys.remove( exportTemplateKey ) selfExportTemplate = self.properties()[exportTemplateKey] otherExportTemplate = other.properties()[exportTemplateKey] # the exportTemplate is a nested list and when loaded into the GUI a list # an be changed to a tuple so that change needs to be ignored if not self.__exportTemplate__eq__( selfExportTemplate, otherExportTemplate ): return False for key in selfPropsKeys: selfProp = self.properties()[key] otherProp = other.properties()[key] # ignore differences between tuples and list if type( selfProp ) in ( list , tuple ): selfProp = list(selfProp) otherProp = list(otherProp) if selfProp != otherProp: return False return True def __ne__( self, other ): """Implements not equal operator using self.__eq__ """ return not self.__eq__( other ) def __repr__(self): return "%s - %s" % (str(self._name), str(self._properties)) def __exportTemplate__eq__(self, selfExportTemplate, otherExportTemplate): """__eq__ method for the export template property. The export template is a list (or tuple) of pairs with format [ path , export template ], and these pairs can be a list or a tuple as well. This method compares two exportTemplates ignoring the difference between list and tuples, so (path1,export1) , (path2,export2)) == [[path1,export1] , [path2,export2]] And the order of the pairs is ignored as well. A unique key is defined to order the list with the 'path', 'export template type' and 'file type'. So ((path1,export1),(path2,export2)) == ((path2,export2),(path1,export1))""" selfExportTemplate = list(selfExportTemplate) otherExporteTemplate = list(otherExportTemplate) if len(selfExportTemplate) != len(otherExportTemplate): return False # sort export template by path, export type and file_type def getSortKey( item ): """Method to define the key to sort the expor templates. The key is a combination of the path, export type and the file type. @return string with path, export type and file type concatenated """ path = item[0] exportTemplate = item[1] exportTemplateType = type(exportTemplate) # exportTemplate can be None, in this case the file type is set to an # empty string fileType = '' if isinstance( exportTemplate , TaskPresetBase ): fileType = exportTemplate.properties().get('file_type','') return '%s%s%s' % ( path , exportTemplateType , fileType ) # order both exportTemplates with a specific key. selfExportTemplate = sorted( selfExportTemplate , key=getSortKey ) otherExportTemplate = sorted( otherExportTemplate , key=getSortKey ) # transform every pair (path,exportTemplate) into lists, # removing the difference between list and tuples again selfExportTemplate = [ list(item) for item in selfExportTemplate ] otherExportTemplate = [ list(item) for item in otherExportTemplate ] # finally compare paths and export templates selfExportTemplatePaths = [ item[0] for item in selfExportTemplate] otherExportTemplatePaths = [ item[0] for item in otherExportTemplate] if selfExportTemplatePaths != otherExportTemplatePaths: return False selfExportTemplateItems = [ item[1] for item in selfExportTemplate] otherExportTemplateItems = [ item[1] for item in otherExportTemplate] if selfExportTemplateItems != otherExportTemplateItems: return False return True
[docs] def name (self): """Return Preset Name""" return self._name
[docs] def setName (self, name): """Set Preset Name""" self._name = name
[docs] def summary(self): """Called by Hiero to get a summary of the preset settings as a string.""" return ""
[docs] def properties(self): """properties() Return the dictionary which is used to persist data within this preset. @return: dict """ return self._properties
[docs] def nonPersistentProperties(self): """nonPersistentProperties() Return the dictionary which contains properties not persisted within the preset. Properties which are only relevant during a single session. @return: dict """ return self._nonPersistentProperties
[docs] def ident (self): """ident(self) Returns a string used for identifying the type of task""" return classBasename(self._parentType)
[docs] def parentType (self): """parentType(self) Returns a the parent Task type for this TaskPreset. @return: TaskPreet class type""" return self._parentType
[docs] def addDefaultResolveEntries(self, resolver): """addDefaultResolveEntries(self, resolver) Create resolve entries for default resolve tokens shared by all task types. @param resolver: ResolveTable object""" resolver.addResolver("{version}", "Version string 'v#', defined by the number (#) set in the Version section of the export dialog", lambda keyword, task: task.versionString()) resolver.addResolver("{project}", "Name of the parent project of the item being processed", lambda keyword, task: task.projectName()) resolver.addResolver("{projectroot}", "Project root path specified in the Project Settings", lambda keyword, task: task.projectRoot()) resolver.addResolver("{hierotemp}", "Temp directory as specified in the Application preferences", lambda keyword, task: ApplicationSettings().value("cacheFolder") ) resolver.addResolver("{timestamp}", "Export start time in 24-hour clock time (HHMM)", lambda keyword, task: task.timeStamp().strftime("%H%M") ) resolver.addResolver("{hour24}", "Export start time hour (24-hour clock)", lambda keyword, task: task.timeStamp().strftime("%H") ) resolver.addResolver("{hour12}", "Export start time hour (12-hour clock)", lambda keyword, task: task.timeStamp().strftime("%I") ) resolver.addResolver("{ampm}", "Locale's equivalent of either AM or PM.", lambda keyword, task: task.timeStamp().strftime("%p") ) resolver.addResolver("{minute}", "Export start time minute [00,59]", lambda keyword, task: task.timeStamp().strftime("%M") ) resolver.addResolver("{second}", "Export start time second [00,61] - '61' accounts for leap/double-leap seconds", lambda keyword, task: task.timeStamp().strftime("%S") ) resolver.addResolver("{day}", "Locale's abbreviated weekday name, [Mon-Sun]", lambda keyword, task: task.timeStamp().strftime("%a") ) resolver.addResolver("{fullday}", "Locale's full weekday name", lambda keyword, task: task.timeStamp().strftime("%A") ) resolver.addResolver("{month}", "Locale's abbreviated month name, [Jan-Dec]", lambda keyword, task: task.timeStamp().strftime("%b") ) resolver.addResolver("{fullmonth}", "Locale's full month name", lambda keyword, task: task.timeStamp().strftime("%B") ) resolver.addResolver("{DD}", "Day of the month as a decimal number, [01,31]", lambda keyword, task: task.timeStamp().strftime("%d") ) resolver.addResolver("{MM}", "Month as a decimal number, [01,12]", lambda keyword, task: task.timeStamp().strftime("%m") ) resolver.addResolver("{YY}", "Year without century as a decimal number [00,99]", lambda keyword, task: task.timeStamp().strftime("%y") ) resolver.addResolver("{YYYY}", "Year with century as a decimal number", lambda keyword, task: task.timeStamp().strftime("%Y") ) resolver.addResolver("{user}", "Current username", lambda keyword, task: getpass.getuser() )
[docs] def addCustomResolveEntries(self, resolver): """addCustomResolveEntries(self, resolver) Impliment this function on custom export tasks to add resolve entries specific to that class. @param resolver: ResolveTable object""" pass
[docs] def addUserResolveEntries(self, resolver): """addUserResolveEntries(self, resolver) Override this function to add user specific resolve tokens. When adding task specific tokens in derrived classes use TaskBase.addCustomResolveEntries(). This is reserved for users to extend the available tokens. @param resolver: ResolveTable object""" pass
[docs] def createResolver(self): """createResolver(self) Instantiate ResolveTable, add default and custom resolve entries return ResolveTable object""" resolver = ResolveTable() self.addDefaultResolveEntries(resolver) self.addCustomResolveEntries(resolver) self.addUserResolveEntries(resolver) return resolver
[docs] def getResolveEntryCount(self): """getResolveEntryCount(self) (DEPRICATED) Return the number of resolve entries in the resolve table""" return self.resolveEntryCount()
[docs] def resolveEntryCount(self): """resolveEntryCount(self) Return the number of resolve entries in the resolve table""" resolver = self.createResolver() return resolver.entryCount()
[docs] def resolveEntryName(self, index): """resolveEntryName(self, index) return ResolveEntry name/token by index""" resolver = self.createResolver() return resolver.entryName(index)
[docs] def resolveEntryDescription(self, index): """resolveEntryDescription(self, index) return ResolveEntry description by index""" resolver = self.createResolver() return resolver.entryDescription(index)
[docs] def setSavePath(self, path): """setSavePath(self, path) Set the save path of the preset in order to ensure it is saved to the path it was loaded from""" self._savePath = path
[docs] def savePath(self): """savePath(self) Return the path from which this preset was loaded. Will return None if this preset was not loaded from file""" return self._savePath
[docs] def setProject(self, project): """Set the Project() object which this preset is associated""" self._project = project
[docs] def project(self): """Return the Project with which this preset is associated. If this is a local preset returns None""" if self._project and not self._project.isNull(): return self._project return None
[docs] def setReadOnly (self, readOnly): """Set Read Only flag on preset, not enforced internally""" self._readOnly = readOnly
[docs] def readOnly (self): """Return the read only flag for this preset""" return self._readOnly
[docs] def markedForDeletion (self): """Return True if this preset is marked for deletion. Delete will be performed at save""" return self._delete
[docs] def setMarkedForDeletion (self, markedForDeletion = True): """Set this preset as marked for deletion. Delete will be performed at save""" self._delete = markedForDeletion
[docs] def skipOffline(self): """skipOffline() Returns True if flag has been set to skip any offline TrackItems @return: bool""" return self._skipOffline
[docs] def setSkipOffline(self, skip): """skipOffline(bool) Set flag to skip offline TrackItems during export. @param bool:""" self._skipOffline = skip
[docs] def setCompsToRender(self, comps): """ Set the list of comps to render. """ self._compsToRender = comps self._compsToSkip = []
[docs] def setCompsToSkip(self, comps): """ Set the list of comps to skip. """ self._compsToRender = [] self._compsToSkip = comps
[docs] def isDeprecated(self): """Determines whether the preset is deprecated. Any configuration that is deprecated should be tested here.""" return False
[docs] def exportsAllTracks(self): """ Check if this preset can export all tracks, including ones which are empty or not enabled. """ return False
#Added for legacy support
[docs]class TaskPreset(TaskPresetBase): """Deprecated - Use TaskPresetBase""" def __init__( self, parentType, presetName ): TaskPresetBase.__init__(self, parentType, presetName) pass
def GetFramerates(): return defaultFrameRates() def getMovProperties(): """ A hardcoded dictionary of all the knobs we are interested in displaying to the user for mov exports. This dictionary contains the union of knob names for all mov codecs. It is assumed that any knob not relevant to a particular codec will be hidden and no value should be set for it. When creating the write node for the export script any knob names with unset values will be ignored. NB the knob names are ordered to match the order within the mov64Writer so that they get written to script in the same order.""" return OrderedDict ([ ("mov64_codec", None), ("mov64_dnxhd_codec_profile", None), ("mov64_dnxhr_codec_profile", None), ("mov_prores_codec_profile", None), ("mov_h264_codec_profile", None), ("mov64_pixel_format", None), ("mov64_quality", None), ("mov64_ycbcr_matrix_type", None), ("dataRange", None), ("mov64_fast_start", None), ("mov64_write_timecode", None), ("mov64_gop_size", None), ("mov64_b_frames", None), ("mov64_limit_bitrate", None), ("mov64_bitrate", None), ("mov64_bitrate_tolerance", None), ("mov64_quality_min", None), ("mov64_quality_max", None) ]) def getMxfProperties(): """ Returns a dictionary of all the knob names for the knobs that should be displayed to the user in the export widget. All the values are set to None by default. NB the knob names are ordered to match the order within the mxfWriter node so that they get written to script in the same order.""" return OrderedDict([ ("mxf_video_codec_knob", None), ("mxf_op_pattern_knob", None), ("mxf_edit_rate_knob", None), ("mxf_codec_profile_knob", None), ("mxf_tape_id_knob", None), ("dataRange", None), ])
[docs]class RenderTaskPreset(TaskPresetBase): """RenderTaskPreset is a specialization of the TaskPreset which contains parameters associated with generating Nuke render output. """
[docs] @staticmethod def AllViews(): """ Get the special cased value for all on the views knob """ return ['all']
def __init__(self, taskType, name, properties): """Initialise presets to default values""" TaskPresetBase.__init__(self, taskType, name) defaultFileType = "dpx" self._properties["file_type"] = defaultFileType self._properties["reformat"] = {"to_type":"None", "scale":1.0, "resize":"width", "center":True, "filter":"Cubic"} self._properties["channels"] = "rgb" # Views property, defaulting to 'all' so the views on the project can # change without the preset needing to be configured self._properties["views"] = RenderTaskPreset.AllViews() # Update preset with loaded data self._properties.update(properties) self._codecSettings = {} # Because this key has changed, its possible that existing presest will exist without this key. # When the _properties dict is updated with the preset information, reformat will be overwritten if not "to_type" in self._properties["reformat"]: self._properties["reformat"]["to_type"] = "None" movFps = GetFramerates() self._setCodecSettings("mov", "mov", "mov", True, getMovProperties()) self._setCodecSettings("dpx", "dpx", "DPX", False, { "datatype" : ("8 bit", Default("10 bit"), "12 bit", "16 bit"), ("Fill", "fill") : False,("Big Endian", "bigEndian") : True, "transfer" : ("(auto detect)","user-defined", "printing density", "linear", "log", "unspecified video", "SMPTE 240M", "CCIR 709-1", "CCIR 601-2 system B/G", "CCIR 601-2 system M", "NTSC", "PAL", "Z linear", "Z homogeneous")}) self._setCodecSettings("dpx", "dpx", "DPX", False, OrderedDict([("datatype" , ("8 bit", Default("10 bit"), "12 bit", "16 bit")), (("Fill", "fill") , False), (("Big Endian", "bigEndian") , True), ("transfer" , ("(auto detect)","user-defined", "printing density", "linear", "log", "unspecified video", "SMPTE 240M", "CCIR 709-1", "CCIR 601-2 system B/G", "CCIR 601-2 system M", "NTSC", "PAL", "Z linear", "Z homogeneous"))])) self._setCodecSettings("dpx", "dpx", "DPX", False, { "datatype" : ("8 bit", Default("10 bit"), "12 bit", "16 bit"), ("Fill", "fill") : False,("Big Endian", "bigEndian") : True, "transfer" : ("(auto detect)","user-defined", "printing density", "linear", "log", "unspecified video", "SMPTE 240M", "CCIR 709-1", "CCIR 601-2 system B/G", "CCIR 601-2 system M", "NTSC", "PAL", "Z linear", "Z homogeneous")}) self._setCodecSettings("exr", "exr", "EXR", False, OrderedDict([ ("datatype" , ("16 bit half", "32 bit float")), ("compression" , ("none", Default("Zip (1 scanline)"),"Zip (16 scanline)", "PIZ Wavelet (32 scanlines)", "RLE", "B44", "B44A", "DWAA", "DWAB")), (("compression level", "dw_compression_level"), FloatRange( 0.0, 500.0, 45.0 )), ("metadata" , ("default metadata","no metadata","default metadata and exr/*", "all metadata except input/*","all metadata")), (("do not attach prefix", "noprefix") , False), ("interleave" , ("channels, layers and views", "channels and layers", "channels")), (("standard layer name format","standard_layer_name_format" ) , False), (("write full layer names", "write_full_layer_names"), False), (("truncate channel names", "truncateChannelNames"), False), (("write ACES compliant EXR", "write_ACES_compliant_EXR"), False)])) self._setCodecSettings("pic", "pic", "PIC", False, OrderedDict() ) self._setCodecSettings("cin", "cin", "Cineon", False, OrderedDict() ) self._setCodecSettings("jpeg", "jpg", "JPEG", False, { ("quality", '_jpeg_quality') : FloatRange(0.0,1.0,0.75)} ) self._setCodecSettings("png", "png", "PNG", False, { "datatype" : ("8 bit", "16 bit")}) self._setCodecSettings("sgi", "sgi", "SGI", False, OrderedDict([("datatype" , ("8 bit", "16 bit")), ("compression" , ("none", Default("RLE")))])) self._setCodecSettings("tiff", "tif", "TIFF", False, OrderedDict([("datatype" , ("8 bit", "16 bit", "32 bit float")), ("compression" , ("none", "PackBits", "LZW", Default("Deflate")))])) self._setCodecSettings("targa", "tga", "Targa", False, { "compression" : ("none", Default("RLE")) }) self._setCodecSettings("mxf", "mxf", "mxf", True, getMxfProperties()) if self._properties["file_type"] not in list(self._codecSettings.keys()): if self._properties["file_type"] == "ffmpeg": self._properties["file_type"] = "mov" elif self._properties["file_type"] == "mov": self._properties["file_type"] = "ffmpeg" else: # arbitrarily pick the first key self._properties["file_type"] = defaultFileType def _setCodecSettings(self, codecType, extension, fullname, isVideo, properties): """Build dictionary of format settings.\n""" self._codecSettings[codecType] = {"extension": extension, "fullname": fullname, "properties": properties, "isVideo": isVideo} def _getCodecSettingsDefault(self, codecType, codecKey): """Search codec settings for a matching codecKey and return a default value. \n @param codecType - format identifier (mov, dpx, jpeg etc..)\n @param codecKey - codec settings key""" dictionary = self._codecSettings[codecType]["properties"] for key, value in dictionary.items(): if hasattr(key, "__iter__"): key = key[1] if key == codecKey: if isinstance(value, FloatRange): return value.default() elif hasattr(value, "__iter__"): for item in value: if isinstance(item, Default): return item elif isinstance(value, (int, float, str)): return value else: return str(value) return None
[docs] def addCustomResolveEntries(self, resolver): """addCustomResolveEntries(self, resolver) RenderTaskPreset adds specialized tokens specific to this type of export, such as {ext} which returns the output format extension. @param resolver: ResolveTable object""" #resolver.addResolver("{height}", "Height component of output format", lambda keyword, task: self.height()) #resolver.addResolver("{width}", "Width component of output format", lambda keyword, task: self.width()) #resolver.addResolver("{pixelaspect}", "Pixel Aspect of output format", lambda keyword, task: self.pixelAspect()) resolver.addResolver("{ext}", "Extension of the file to be output", lambda keyword, task: self.extension())
#def height (self): # if "height" in self._properties["reformat"]: # return self._properties["reformat"]["height"] # elif isinstance(self._item, hiero.core.Sequence): # return self._sequence.format().height() # else: # self._source.height() #def width (self): # if "width" in self._properties["reformat"]: # return self._properties["reformat"]["width"] # elif isinstance(self._item, hiero.core.Sequence): # return self._sequence.format().width() # else: # self._source.width() #def pixelAspect (self): # if "pixelAspect" in self._properties["reformat"]: # return self._properties["reformat"]["pixelAspect"] # elif isinstance(self._item, hiero.core.Sequence): # return self._sequence.format().pixelAspect() # else: # self._source.pixelAspect()
[docs] def summary(self): properties = [] fileType = self._properties["file_type"] if fileType in self._properties: properties = self._properties[fileType] codecproperties = [] # This is to prevent the hideous quicktime settings hex string appearing in the summary for key, value in properties.items(): if not (fileType=="mov" and key == "settings"): codecproperties.append(value) return str("(%s - %s)" % (fileType, ", ".join(str(value) for value in codecproperties))) else: return str("(%s)" % fileType)
[docs] def extension(self): return self.codecSettings()['extension']
[docs] def codecName(self): return self.codecSettings()['fullname']
[docs] def codecProperties(self): return self.codecSettings()['properties']
[docs] def codecSettings(self): return self._codecSettings[self._properties['file_type']]
class RenderTaskPresetBase(RenderTaskPreset): """RenderTaskPresetBase (Deprecated) - Use RenderTaskPreset""" pass
[docs]class FolderTask(TaskBase): """ Task which just creates an empty folder. """ def __init__(self, initDict): TaskBase.__init__(self, initDict)
[docs]class FolderTaskPreset(TaskPresetBase): """ Preset which can be used for creating an empty folder. """ def __init__(self, name, properties): TaskPresetBase.__init__(self, FolderTask, name) self.properties().update(properties)
def tagsFromSelection(selection, includeChildren=False, includeParents=False): """Returns a list of tuples for tag/parent type pairs""" def _items(selection): # Generator function for extracting the actual item from ItemWrappers for item in selection: if isinstance(item, ItemWrapper): if not item.ignore(): yield item.item() else: yield item tags = [] for item in _items(selection): if isinstance(item, TrackItem): # Collect all tags from trackItem tags.extend([( tag, TrackItem ) for tag in item.tags()]) if includeParents: tags.extend(tagsFromSelection([item.parent()], includeChildren=False, includeParents=True)) elif isinstance(item, (VideoTrack, AudioTrack)): tags.extend([( tag, type(item) ) for tag in item.tags()]) if includeChildren: for trackItem in item: tags.extend(tagsFromSelection([trackItem,], includeChildren)) elif includeParents: tags.extend(tagsFromSelection([item.parent()], includeChildren=False, includeParents=True)) elif isinstance(item, Sequence): # Traverse sequence and collect any tags sequence = item tags.extend([(tag, Sequence ) for tag in sequence.tags()]) if includeChildren: for track in sequence.videoTracks() + sequence.audioTracks(): tags.extend(tagsFromSelection([track,], includeChildren)) elif isinstance(item, Clip): tags.extend([( tag, Clip ) for tag in item.tags() ] ) return tags def mediaSourceExportReadPath(source, useFirstFrame): """ Get the path to include in exports from a media source. If the source is an nk script return the comp write path. If useFirstFrame is True, for file sequences will return e.g. clip.0001.dpx rather than clip.%04d.dpx """ path = source.firstpath() if useFirstFrame else source.fileinfos()[0].filename() try: compInfo = CompSourceInfo(source) if compInfo.isComp(): path = compInfo.writePath if useFirstFrame: path = path % compInfo.firstFrame except: pass return path def mediaSourceExportFileHead(source): """ Get the filename head from a media source, fixing for use as a keyword. """ head = source.filenameHead() # If the filename is e.g. bob.%03.dpx, filenameHead() will return everything up the frame number, including the # '. just before it, so 'bob.'. This is not very helpful when resolving the keywords, so check for the string # ending with a '.' and if so remove it. if head.endswith('.'): head = head[:-1] return head # some script to run in Nuke to get the ffmpeg formats, codecs and macro block decision lists #n = nuke.createNode("Write") # #def printEnum(k): # print "[" # values = k.values() # for i in values: # print "\"" + i + "\", " # print "]" # #n.knobs()["file_type"].setValue("ffmpeg") # #printEnum(n.knobs()["format"]) #printEnum(n.knobs()["codec"]) #printEnum(n.knobs()["mbDecision"])