diff --git a/hooks/tk-multi-publish/primary_pre_publish.py b/hooks/tk-multi-publish/primary_pre_publish.py index 71afc18..2d12233 100644 --- a/hooks/tk-multi-publish/primary_pre_publish.py +++ b/hooks/tk-multi-publish/primary_pre_publish.py @@ -1,11 +1,11 @@ # Copyright (c) 2013 Shotgun Software Inc. -# +# # CONFIDENTIAL AND PROPRIETARY -# -# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit # Source Code License included in this distribution package. See LICENSE. -# By accessing, using, copying or modifying this work you indicate your -# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. import os @@ -16,28 +16,30 @@ TK_FRAMEWORK_PERFORCE_NAME = "tk-framework-perforce_v0.x.x" + class PrimaryPrePublishHook(Hook): """ Single hook that implements pre-publish of the primary task - """ + """ + def execute(self, task, work_template, progress_cb, **kwargs): """ Main hook entry point :param task: Primary task to be pre-published. This is a dictionary containing the following keys: - { + { item: Dictionary - This is the item returned by the scan hook - { + This is the item returned by the scan hook + { name: String description: String type: String other_params: Dictionary } - + output: Dictionary - This is the output as defined in the configuration - the - primary output will always be named 'primary' + This is the output as defined in the configuration - the + primary output will always be named 'primary' { name: String publish_template: template @@ -47,25 +49,25 @@ def execute(self, task, work_template, progress_cb, **kwargs): :param work_template: template This is the template defined in the config that represents the current work file - + :param progress_cb: Function A progress callback to log progress during pre-publish. Call: - + progress_cb(percentage, msg) - + to report progress to the UI - :returns: List - A list of non-critical problems that should be + :returns: List + A list of non-critical problems that should be reported to the user but not stop the publish. - + :raises: Hook should raise a TankError if the primary task can't be published! """ - + # get the engine name from the parent object (app/engine/etc.) engine_name = self.parent.engine.name - + # depending on engine: if engine_name == "tk-3dsmax": return self._do_3dsmax_pre_publish(task, work_template, progress_cb) @@ -74,142 +76,186 @@ def execute(self, task, work_template, progress_cb, **kwargs): elif engine_name == "tk-maya": return self._do_maya_pre_publish(task, work_template, progress_cb) elif engine_name == "tk-photoshop": - return self._do_photoshop_pre_publish(task, work_template, progress_cb) + return self._do_photoshop_pre_publish(task, work_template, progress_cb) + elif engine_name == "tk-photoshopcc": + return self._do_photoshopcc_pre_publish(task, work_template, progress_cb) else: - raise TankError("Unable to perform pre-publish for unhandled engine %s" % engine_name) - + raise TankError("Unable to perform pre-publish for unhandled engine %s" % engine_name) + def _do_3dsmax_pre_publish(self, task, work_template, progress_cb): """ Do 3ds Max primary pre-publish/scene validation - + :param task: The primary task to pre-publish :param work_template: The template that matches the current work file :param progress_cb: The progress callback to report all progress through - - :returns: A list of strings representing any non-critical problems that + + :returns: A list of strings representing any non-critical problems that were found during pre-processing. """ from Py3dsMax import mxs - + progress_cb(0.0, "Validating current scene", task) - + # get the current scene file: scene_file = os.path.abspath(os.path.join(mxs.maxFilePath, mxs.maxFileName)) - + progress_cb(25) - + # validate the work path: if not work_template.validate(scene_file): raise TankError("File '%s' is not a valid work path, unable to publish!" % scene_file) - + # Do any additional validation of the scene/primary task: p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) p4 = p4_fw.connection.connect() - p4_fw.util.open_file_for_edit(p4, scene_file, test_only=True) - + p4_fw.util.open_file_for_edit(p4, scene_file, test_only=True) + progress_cb(100) - - return [] # no errors - + + return [] # no errors + def _do_3dsmaxplus_pre_publish(self, task, work_template, progress_cb): """ Do 3ds Max with MaxPlus primary pre-publish/scene validation - + :param task: The primary task to pre-publish :param work_template: The template that matches the current work file :param progress_cb: The progress callback to report all progress through - - :returns: A list of strings representing any non-critical problems that + + :returns: A list of strings representing any non-critical problems that were found during pre-processing. """ import MaxPlus - + progress_cb(0.0, "Validating current scene", task) - + # get the current scene file: scene_file = MaxPlus.FileManager.GetFileNameAndPath() - + progress_cb(25) - + # validate the work path: if not work_template.validate(scene_file): raise TankError("File '%s' is not a valid work path, unable to publish!" % scene_file) - + # Do any additional validation of the scene/primary task: p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) p4 = p4_fw.connection.connect() - p4_fw.util.open_file_for_edit(p4, scene_file, test_only=True) - + p4_fw.util.open_file_for_edit(p4, scene_file, test_only=True) + progress_cb(100) - - return [] # no errors + + return [] # no errors + def _do_maya_pre_publish(self, task, work_template, progress_cb): """ Do Maya primary pre-publish/scene validation - + :param task: The primary task to pre-publish :param work_template: The template that matches the current work file :param progress_cb: The progress callback to report all progress through - - :returns: A list of strings representing any non-critical problems that - were found during pre-processing. + + :returns: A list of strings representing any non-critical problems that + were found during pre-processing. """ import maya.cmds as cmds - + progress_cb(0.0, "Validating current scene", task) - + # get the current scene file: scene_file = cmds.file(query=True, sn=True) if scene_file: scene_file = os.path.abspath(scene_file) - + progress_cb(25) - + # validate the work path: if not work_template.validate(scene_file): raise TankError("File '%s' is not a valid work path, unable to publish!" % scene_file) - + # Do any additional validation of the scene/primary task: p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) p4 = p4_fw.connection.connect() p4_fw.util.open_file_for_edit(p4, scene_file, test_only=True) - + progress_cb(100) - - return [] # no errors - + + return [] # no errors + def _do_photoshop_pre_publish(self, task, work_template, progress_cb): """ Do Photoshop primary pre-publish/scene validation - + :param task: The primary task to pre-publish :param work_template: The template that matches the current work file :param progress_cb: The progress callback to report all progress through - - :returns: A list of strings representing any non-critical problems that - were found during pre-processing. + + :returns: A list of strings representing any non-critical problems that + were found during pre-processing. """ import photoshop - + progress_cb(0.0, "Validating current scene", task) - + # get the current scene file: doc = photoshop.app.activeDocument if doc is None: raise TankError("There is no currently active document!") - + scene_file = doc.fullName.nativePath - + # validate the work path: if not work_template.validate(scene_file): raise TankError("File '%s' is not a valid work path, unable to publish!" % scene_file) - + # Do any additional validation of the scene/primary task: p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) p4 = p4_fw.connection.connect() p4_fw.util.open_file_for_edit(p4, scene_file, test_only=True) - + progress_cb(100) - - return [] # no errors - + + return [] # no errors + + def _do_photoshopcc_pre_publish(self, task, work_template, progress_cb): + """ + Do Photoshop primary pre-publish/scene validation + + :param task: The primary task to pre-publish + :param work_template: The template that matches the current work file + :param progress_cb: The progress callback to report all progress through + + :returns: A list of strings representing any non-critical problems that + were found during pre-processing. + """ + # import photoshop + adobe = self.parent.engine.adobe + progress_cb(0.0, "Validating current scene", task) + + # get the current scene file: + try: + doc = adobe.app.activeDocument + except RuntimeError: + raise TankError("There is no active document!") + + if not doc.saved: + raise TankError("Please Save your file before Publishing") + + try: + scene_path = doc.fullName.fsName + except RuntimeError: + raise TankError("Please save your file before publishing!") + + # validate the work path: + if not work_template.validate(scene_path): + raise TankError("File '%s' is not a valid work path, unable to publish!" % scene_path) + + # Do any additional validation of the scene/primary task: + p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) + p4 = p4_fw.connection.connect() + p4_fw.util.open_file_for_edit(p4, scene_path, test_only=True) + + progress_cb(100) + + return [] # no errors diff --git a/hooks/tk-multi-publish/primary_publish.py b/hooks/tk-multi-publish/primary_publish.py index f43d745..8601d1b 100644 --- a/hooks/tk-multi-publish/primary_publish.py +++ b/hooks/tk-multi-publish/primary_publish.py @@ -1,11 +1,11 @@ # Copyright (c) 2013 Shotgun Software Inc. -# +# # CONFIDENTIAL AND PROPRIETARY -# -# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit # Source Code License included in this distribution package. See LICENSE. -# By accessing, using, copying or modifying this work you indicate your -# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights # not expressly granted therein are reserved by Shotgun Software Inc. import sgtk @@ -13,73 +13,73 @@ from sgtk import TankError import os -import tempfile -import uuid TK_FRAMEWORK_PERFORCE_NAME = "tk-framework-perforce_v0.x.x" + class PrimaryPublishHook(Hook): """ Single hook that implements publish of the primary task - """ + """ + def execute(self, task, work_template, comment, thumbnail_path, sg_task, progress_cb, **kwargs): """ Main hook entry point :param task: Primary task to be published. This is a dictionary containing the following keys: - { + { item: Dictionary - This is the item returned by the scan hook - { + This is the item returned by the scan hook + { name: String description: String type: String other_params: Dictionary } - + output: Dictionary - This is the output as defined in the configuration - the - primary output will always be named 'primary' + This is the output as defined in the configuration - the + primary output will always be named 'primary' { name: String publish_template: template tank_type: String } } - + :param work_template: template This is the template defined in the config that represents the current work file - + :param comment: String The comment provided for the publish - + :param thumbnail: Path string The default thumbnail provided for the publish - + :param sg_task: Dictionary (shotgun entity description) - The shotgun task to use for the publish - + The shotgun task to use for the publish + :param progress_cb: Function A progress callback to log progress during pre-publish. Call: - + progress_cb(percentage, msg) - + to report progress to the UI - + :returns: Path String Hook should return the path of the primary publish so that it can be passed as a dependency to all secondary publishes - - :raises: Hook should raise a TankError if publish of the + + :raises: Hook should raise a TankError if publish of the primary task fails """ # get the engine name from the parent object (app/engine/etc.) engine_name = self.parent.engine.name - + # load the perforce framework: p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) - + # create a publisher instance depending on the engine: publisher = None if engine_name == "tk-3dsmax": @@ -90,25 +90,29 @@ def execute(self, task, work_template, comment, thumbnail_path, sg_task, progres publisher = MayaPublisher(self.parent, p4_fw) elif engine_name == "tk-photoshop": publisher = PhotoshopPublisher(self.parent, p4_fw) - + elif engine_name == "tk-photoshopcc": + publisher = PhotoshopCCPublisher(self.parent, p4_fw) + if publisher: return publisher.do_publish(task, work_template, comment, thumbnail_path, sg_task, progress_cb) else: raise TankError("Unable to perform publish for unhandled engine %s" % engine_name) - + + class PublisherBase(object): """ Publisher base class - implements the main publish functionality with virtual methods provided to be overwridden in derived, application specific classes. """ + def __init__(self, bundle, p4_fw): """ Construction """ self.parent = bundle self.p4_fw = p4_fw - + def do_publish(self, task, work_template, comment, thumbnail_path, sg_task, progress_cb): """ Publish the scene into Perforce and register the Publish data ready @@ -116,7 +120,7 @@ def do_publish(self, task, work_template, comment, thumbnail_path, sg_task, prog """ # get the scene path: scene_path = self._get_scene_path() - + if not work_template.validate(scene_path): raise TankError("File '%s' is not a valid work path, unable to publish!" % scene_path) @@ -127,11 +131,11 @@ def do_publish(self, task, work_template, comment, thumbnail_path, sg_task, prog # open a Perforce connection: progress_cb(5.0, "Connecting to Perforce...") p4 = self.p4_fw.connection.connect() - + # Ensure the file is checked out/added to depot: progress_cb(10.0, "Ensuring file is checked out...") - self.p4_fw.util.open_file_for_edit(p4, scene_path) - + self.p4_fw.util.open_file_for_edit(p4, scene_path) + # save the scene using the save_scene_fn passed in: progress_cb(30.0, "Saving the scene") self.parent.log_debug("Saving the scene...") @@ -141,11 +145,11 @@ def do_publish(self, task, work_template, comment, thumbnail_path, sg_task, prog # changelist for all files being published: progress_cb(50.0, "Creating new Perforce changelist...") new_change = self.p4_fw.util.create_change(p4, comment or "Shotgun publish") - + # store the change on the primary item 'other_params' so that it can be # found by the following secondary publish hook: task["item"].setdefault("other_params", dict())["p4_change"] = new_change - + # and add the file to this change: progress_cb(60.0, "Adding scene file to change...") self.p4_fw.util.add_to_change(p4, new_change, scene_path) @@ -153,20 +157,20 @@ def do_publish(self, task, work_template, comment, thumbnail_path, sg_task, prog # next, we want to save the publish metadata so that the sync daemon # can pick it up and add to the publish record: progress_cb(70.0, "Storing publish data...") - publish_data = {"thumbnail_path":thumbnail_path, - "dependency_paths":dependencies, - "task":sg_task, - "comment":comment, - "context":self.parent.context, - "published_file_type":task["output"]["tank_type"], - "created_by":self.parent.context.user + publish_data = {"thumbnail_path": thumbnail_path, + "dependency_paths": dependencies, + "task": sg_task, + "comment": comment, + "context": self.parent.context, + "published_file_type": task["output"]["tank_type"], + "created_by": self.parent.context.user } self.p4_fw.store_publish_data(scene_path, publish_data) progress_cb(100) - + return scene_path - + def _get_scene_path(self): """ Return the current scene path @@ -178,13 +182,14 @@ def _save(self): Save the current scene """ raise NotImplementedError() - + def _find_scene_dependencies(self): """ Find dependencies for the current scene """ return [] + class MaxPublisher(PublisherBase): """ 3ds Max specific instance of the Publisher class @@ -205,6 +210,7 @@ def _save(self): scene_path = os.path.abspath(os.path.join(mxs.maxFilePath, mxs.maxFileName)) mxs.saveMaxFile(scene_path) + class MaxPlusPublisher(PublisherBase): """ 3ds Max with MaxPlus specific instance of the Publisher class @@ -212,7 +218,7 @@ class MaxPlusPublisher(PublisherBase): def _get_scene_path(self): """ - Return the current scene path for 3ds Max + Return the current scene path for 3ds Max """ import MaxPlus return MaxPlus.FileManager.GetFileNameAndPath() @@ -225,6 +231,7 @@ def _save(self): scene_path = MaxPlus.FileManager.GetFileNameAndPath() MaxPlus.FileManager.Save(scene_path) + class MayaPublisher(PublisherBase): """ Maya specific instance of the Publisher class @@ -232,10 +239,10 @@ class MayaPublisher(PublisherBase): def _get_scene_path(self): """ - Return the current scene path for Maya + Return the current scene path for Maya """ import maya.cmds as cmds - return os.path.abspath(cmds.file(query=True, sn=True)) + return os.path.abspath(cmds.file(query=True, sn=True)) def _save(self): """ @@ -243,19 +250,19 @@ def _save(self): """ import maya.cmds as cmds cmds.file(save=True, force=True) - + def _find_scene_dependencies(self): """ Find dependencies for the current scene """ import maya.cmds as cmds - # default implementation looks for references and + # default implementation looks for references and # textures (file nodes) and returns any paths that # match a template defined in the configuration ref_paths = set() - - # first let's look at maya references + + # first let's look at maya references ref_nodes = cmds.ls(references=True) for ref_node in ref_nodes: # get the path: @@ -265,8 +272,8 @@ def _find_scene_dependencies(self): ref_path = ref_path.replace("/", os.path.sep) if ref_path: ref_paths.add(ref_path) - - # now look at file texture nodes + + # now look at file texture nodes for file_node in cmds.ls(l=True, type="file"): # ensure this is actually part of this scene and not referenced if cmds.referenceQuery(file_node, isNodeReferenced=True): @@ -279,7 +286,7 @@ def _find_scene_dependencies(self): texture_path = cmds.getAttr("%s.fileTextureName" % file_node).replace("/", os.path.sep) if texture_path: ref_paths.add(texture_path) - + # now, for each reference found, build a list of the ones # that resolve against a template: dependency_paths = [] @@ -291,21 +298,23 @@ def _find_scene_dependencies(self): break return dependency_paths - + + class PhotoshopPublisher(PublisherBase): """ Photoshop specific instance of the Publisher class """ + def _get_scene_path(self): """ Return the current scene/document path for Photoshop """ import photoshop - + doc = photoshop.app.activeDocument if not doc: raise TankError("There is no currently active document!") - + # get scene path return doc.fullName.nativePath @@ -314,9 +323,46 @@ def _save(self): Save the current scene """ import photoshop - + doc = photoshop.app.activeDocument if not doc: raise TankError("There is no currently active document!") - + + doc.save() + + +class PhotoshopCCPublisher(PublisherBase): + """ + Photoshop specific instance of the Publisher class + """ + + def _get_scene_path(self): + """ + Return the current scene/document path for Photoshop + """ + adobe = self.parent.engine.adobe + + try: + doc = adobe.app.activeDocument + except RuntimeError: + raise TankError("There is no active document!") + + try: + scene_path = doc.fullName.fsName + except RuntimeError: + raise TankError("Please save your file before publishing!") + + return scene_path + + def _save(self): + """ + Save the current scene + """ + adobe = self.parent.engine.adobe + + try: + doc = adobe.app.activeDocument + except RuntimeError: + raise TankError("There is no active document!") + doc.save() diff --git a/hooks/tk-multi-publish/scan_scene_tk-photoshopcc.py b/hooks/tk-multi-publish/scan_scene_tk-photoshopcc.py new file mode 100644 index 0000000..c3efeca --- /dev/null +++ b/hooks/tk-multi-publish/scan_scene_tk-photoshopcc.py @@ -0,0 +1,104 @@ +# Copyright (c) 2013 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +# import photoshop + +import sgtk +from sgtk import Hook +from sgtk import TankError + +logger = sgtk.platform.get_logger(__name__) + +class ScanSceneHook(Hook): + """ + Hook to scan scene for items to publish + """ + + def execute(self, **kwargs): + """ + Main hook entry point + :returns: A list of any items that were found to be published. + Each item in the list should be a dictionary containing + the following keys: + { + type: String + This should match a scene_item_type defined in + one of the outputs in the configuration and is + used to determine the outputs that should be + published for the item + + name: String + Name to use for the item in the UI + + description: String + Description of the item to use in the UI + + selected: Bool + Initial selected state of item in the UI. + Items are selected by default. + + required: Bool + Required state of item in the UI. If True then + item will not be deselectable. Items are not + required by default. + + other_params: Dictionary + Optional dictionary that will be passed to the + pre-publish and publish hooks + } + """ + + items = [] + adobe = self.parent.engine.adobe + + # get the main scene: + try: + doc = adobe.app.activeDocument + except RuntimeError: + raise TankError("There is no active document!") + + if not doc.saved: + raise TankError("Please Save your file before Publishing") + + try: + scene_path = doc.fullName.fsName + except RuntimeError: + raise TankError("Please save your file before publishing!") + + name = os.path.basename(scene_path) + + # create the primary item - this will match the primary output 'scene_item_type': + items.append({"type": "work_file", "name": name}) + + # add secondary item for sending to review: + items.append({"type": "send_to_review", "name": name}) + + # always add a secondary item to allow user to commit all changes to Perforce: + # Note: only need one of these as it submits all published files + items.append({"type": "perforce_submit", "name": "All Published Files"}) + + # finally, look for specific layers that we can handle: + layers = doc.artLayers + + for layer in layers: + # ignore layers that aren't visible: + if not layer.visible: + continue + + # filter for just those layers that we can handle: + layer_name = layer.name.encode("utf8") + if layer_name not in ["diffuse", "specular", "normal"]: + continue + + items.append({"type": "layer", "name": layer_name, + "description": "Export as the %s texture" % layer_name}) + + return items diff --git a/hooks/tk-multi-publish/secondary_pre_publish_tk-photoshopcc.py b/hooks/tk-multi-publish/secondary_pre_publish_tk-photoshopcc.py new file mode 100644 index 0000000..931d5a5 --- /dev/null +++ b/hooks/tk-multi-publish/secondary_pre_publish_tk-photoshopcc.py @@ -0,0 +1,155 @@ +# Copyright (c) 2013 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +from itertools import chain + +# import photoshop + +import sgtk +from sgtk import Hook +from sgtk import TankError + +TK_FRAMEWORK_PERFORCE_NAME = "tk-framework-perforce_v0.x.x" + + +class PrePublishHook(Hook): + """ + Single hook that implements pre-publish functionality + """ + + def execute(self, tasks, work_template, progress_cb, **kwargs): + """ + Main hook entry point + :param tasks: List of tasks to be pre-published. Each task is be a + dictionary containing the following keys: + { + item: Dictionary + This is the item returned by the scan hook + { + name: String + description: String + type: String + other_params: Dictionary + } + + output: Dictionary + This is the output as defined in the configuration - the + primary output will always be named 'primary' + { + name: String + publish_template: template + tank_type: String + } + } + + :param work_template: template + This is the template defined in the config that + represents the current work file + + :param progress_cb: Function + A progress callback to log progress during pre-publish. Call: + + progress_cb(percentage, msg) + + to report progress to the UI + + :returns: A list of any tasks that were found which have problems that + need to be reported in the UI. Each item in the list should + be a dictionary containing the following keys: + { + task: Dictionary + This is the task that was passed into the hook and + should not be modified + { + item:... + output:... + } + + errors: List + A list of error messages (strings) to report + } + """ + results = [] + adobe = self.parent.engine.adobe + + doc = adobe.app.activeDocument + + p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) + p4 = p4_fw.connection.connect() + + # validate tasks: + for task in tasks: + item = task["item"] + output = task["output"] + errors = [] + + # report progress: + progress_cb(0, "Validating", task) + + # pre-publish item here, e.g. + if output["name"] == "p4_submit": + # this is always valid! + pass + elif output["name"] == "export_layers": + # check that the specified layer is still valid: + layer_errors = self.__validate_layer(doc, item["name"], work_template, output["publish_template"], p4, p4_fw) + if layer_errors: + errors += layer_errors + + elif output["name"] == "send_to_review": + # this is always valid! + pass + else: + errors.append("Don't know how to publish this item!") + + # if there is anything to report then add to result + if len(errors) > 0: + # add result: + results.append({"task": task, "errors": errors}) + + progress_cb(100) + + return results + + def __validate_layer(self, doc, layer_name, work_template, publish_template, p4, p4_fw): + """ + Validate the specified layer: + """ + errors = [] + + scene_file = doc.fullName.nativePath + layer = doc.artLayers.getByName(layer_name) + + # check layer actually exists! + if not layer: + errors.append("Layer '%s' could not be found!" % layer_name) + + # work out the export path for the layer: + layer_short_name = {"diffuse": "c", "normal": "n", "specular": "s"}.get(layer_name) + export_path = None + try: + fields = work_template.get_fields(scene_file) + fields = dict(chain(fields.items(), self.parent.context.as_template_fields(publish_template).items())) + fields["TankType"] = "%s Texture" % layer_name.capitalize() + fields["layer_short_name"] = layer_short_name + + export_path = publish_template.apply_fields(fields).encode("utf8") + except TankError, e: + errors.append("Failed to construct export path for layer '%s': %s" % (layer_name, e)) + + if export_path: + # check that the file can be opened for edit in Perforce: + try: + p4_fw.util.open_file_for_edit(p4, export_path, test_only=True) + except TankError, e: + errors.append("%s" % e) + + return errors diff --git a/hooks/tk-multi-publish/secondary_publish_tk-photoshopcc.py b/hooks/tk-multi-publish/secondary_publish_tk-photoshopcc.py new file mode 100644 index 0000000..804e12e --- /dev/null +++ b/hooks/tk-multi-publish/secondary_publish_tk-photoshopcc.py @@ -0,0 +1,397 @@ +# Copyright (c) 2013 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +import os +import shutil +import tempfile +import uuid +import re +from itertools import chain + +# import photoshop + +import sgtk +from sgtk import Hook +from sgtk import TankError + +TK_FRAMEWORK_PERFORCE_NAME = "tk-framework-perforce_v0.x.x" + + +class PublishHook(Hook): + """ + Single hook that implements publish functionality for secondary tasks + """ + + def execute(self, tasks, work_template, comment, thumbnail_path, sg_task, primary_task, primary_publish_path, progress_cb, **kwargs): + """ + Main hook entry point + :param tasks: List of secondary tasks to be published. Each task is a + dictionary containing the following keys: + { + item: Dictionary + This is the item returned by the scan hook + { + name: String + description: String + type: String + other_params: Dictionary + } + + output: Dictionary + This is the output as defined in the configuration - the + primary output will always be named 'primary' + { + name: String + publish_template: template + tank_type: String + } + } + + :param work_template: template + This is the template defined in the config that + represents the current work file + + :param comment: String + The comment provided for the publish + + :param thumbnail: Path string + The default thumbnail provided for the publish + + :param sg_task: Dictionary (shotgun entity description) + The shotgun task to use for the publish + + :param primary_publish_path: Path string + This is the path of the primary published file as returned + by the primary publish hook + + :param progress_cb: Function + A progress callback to log progress during pre-publish. Call: + + progress_cb(percentage, msg) + + to report progress to the UI + + :param primary_task: The primary task that was published by the primary publish hook. Passed + in here for reference. This is a dictionary in the same format as the + secondary tasks above. + + :returns: A list of any tasks that had problems that need to be reported + in the UI. Each item in the list should be a dictionary containing + the following keys: + { + task: Dictionary + This is the task that was passed into the hook and + should not be modified + { + item:... + output:... + } + + errors: List + A list of error messages (strings) to report + } + """ + p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) + + # open a connection to Perforce: + p4 = p4_fw.connection.connect() + + # find the changelist containing the primary publish file that was + # created during the primary publish phase. + primary_change = primary_task["item"].get("other_params", {}).get("p4_change") + if not primary_change: + raise TankError("Failed to find the Perforce change in the secondary publish hook!") + + results = [] + + # we want to keep track of all files being published + # so that we can add them to the Perforce change at + # the end. + secondary_publish_files = [] + p4_submit_task = None + + # publish all tasks except the "p4_submit" task: + for task in tasks: + item = task["item"] + output = task["output"] + errors = [] + + if output["name"] == "p4_submit": + # we'll handle this later: + p4_submit_task = task + continue + + # report progress: + progress_cb(0, "Publishing", task) + + if output["name"] == "export_layers": + # publish the layer as a tif: + export_errors = self.__publish_layer_as_tif(item["name"], + work_template, + output["publish_template"], + primary_publish_path, + sg_task, + comment, + p4, + p4_fw, + primary_change, + progress_cb) + if export_errors: + errors += export_errors + elif output["name"] == "send_to_review": + # register review data for the current document: + review_errors = self.__send_to_review(primary_publish_path, sg_task, comment, p4, p4_fw, progress_cb) + if review_errors: + errors += review_errors + else: + # don't know how to publish this output types! + errors.append("Don't know how to publish this item!") + + # if there is anything to report then add to result + if len(errors) > 0: + # add result: + results.append({"task": task, "errors": errors}) + + progress_cb(100) + + # now, if we need to, lets commit the change to perforce: + if p4_submit_task: + errors = [] + + progress_cb(0, "Publishing", task) + + if primary_change is None: + errors.append("Failed to find the Perforce change containing the file '%s'" % primary_publish_path) + else: + progress_cb(10, "Submitting change '%s'" % primary_change) + p4_fw.util.submit_change(p4, primary_change) + + # if there is anything to report then add to result + if len(errors) > 0: + # add result: + results.append({"task": p4_submit_task, "errors": errors}) + + progress_cb(100) + + return results + + def __publish_layer_as_tif(self, layer_name, work_template, publish_template, primary_publish_path, + sg_task, comment, p4, p4_fw, change, progress_cb): + """ + Publish the specified layer + """ + adobe = self.parent.engine.adobe + errors = [] + MAX_THUMB_SIZE = 512 + + # publish type will be driven from the layer name: + publish_type = "%s Texture" % layer_name.capitalize() + + # generate the export path using the correct template together + # with the fields extracted from the work template: + export_path = None + + progress_cb(10, "Building output path") + + layer_short_name = {"diffuse": "c", "normal": "n", "specular": "s"}.get(layer_name) + try: + fields = work_template.get_fields(primary_publish_path) + fields = dict(chain(fields.items(), self.parent.context.as_template_fields(publish_template).items())) + fields["TankType"] = publish_type + fields["layer_short_name"] = layer_short_name + + export_path = publish_template.apply_fields(fields).encode("utf8") + except TankError, e: + errors.append("Failed to construct export path for layer '%s': %s" % (layer_name, e)) + return errors + + # ensure the export folder exists: + export_folder = os.path.dirname(export_path) + self.parent.ensure_folder_exists(export_folder) + + file_in_perforce = False + if os.path.exists(export_path): + # check out the file if it's already in Perforce: + progress_cb(15, "Checking out file from Perforce") + try: + p4_fw.util.open_file_for_edit(p4, export_path) + except TankError, e: + errors.append("%s" % e) + return errors + file_in_perforce = True + + # get a path in the temp dir to use for the thumbnail: + thumbnail_path = os.path.join(tempfile.gettempdir(), "%s_sgtk.png" % uuid.uuid4().hex) + + # set unit system to pixels: + original_ruler_units = adobe.app.preferences.rulerUnits + pixel_units = adobe.StaticObject('com.adobe.photoshop.Units', 'PIXELS') + adobe.app.preferences.rulerUnits = pixel_units + + try: + active_doc = adobe.app.activeDocument + orig_name = active_doc.name + width_str = active_doc.width + height_str = active_doc.height + + # calculate the thumbnail doc size: + doc_width = doc_height = 0 + exp = re.compile("^(?P[0-9]+) px$") + mo = exp.match(width_str) + if mo: + doc_width = int(mo.group("value")) + mo = exp.match(height_str) + if mo: + doc_height = int(mo.group("value")) + + thumb_width = thumb_height = 0 + if doc_width and doc_height: + max_sz = max(doc_width, doc_height) + if max_sz > MAX_THUMB_SIZE: + scale = min(float(MAX_THUMB_SIZE) / float(max_sz), 1.0) + thumb_width = max(min(int(doc_width * scale), doc_width), 1) + thumb_height = max(min(int(doc_height * scale), doc_height), 1) + + # set up the export options and get a file object: + layer_file = adobe.RemoteObject('flash.filesystem::File', export_path) + tiff_save_options = adobe.RemoteObject('com.adobe.photoshop::TiffSaveOptions') + tiff_save_options.layers = False + + # set up the thumbnail options and get a file object: + thumbnail_file = adobe.RemoteObject('flash.filesystem::File', thumbnail_path) + png_save_options = adobe.RemoteObject('com.adobe.photoshop::PNGSaveOptions') + + close_save_options = adobe.flexbase.requestStatic('com.adobe.photoshop.SaveOptions', 'DONOTSAVECHANGES') + + progress_cb(20, "Exporting %s layer" % layer_name) + + # duplicate doc + doc_name, doc_sfx = os.path.splitext(orig_name) + layer_doc_name = "%s_%s.%s" % (doc_name, layer_name, doc_sfx) + layer_doc = active_doc.duplicate(layer_doc_name) + try: + # set layer visibility + layers = layer_doc.artLayers + for layer in [layers.index(li) for li in xrange(layers.length)]: + layer.visible = (layer.name == layer_name) + + # flatten + layer_doc.flatten() + + # save: + layer_doc.saveAs(layer_file, tiff_save_options, True) + + progress_cb(60, "Exporting thumbnail") + + # resize for thumbnail + if thumb_width and thumb_height: + layer_doc.resizeImage("%d px" % thumb_width, "%d px" % thumb_height) + + # save again (as thumbnail) + layer_doc.saveAs(thumbnail_file, png_save_options, True) + + finally: + # close the doc: + layer_doc.close(close_save_options) + + # add publish file to the change: + # Note, if it looks like this is taking ages it's probably just because + # the progress bar hasn't updated between this and storing the publish + # data, which can take a bit of time + progress_cb(80, "Adding to Perforce change %s" % change) + if not file_in_perforce: + try: + p4_fw.util.open_file_for_edit(p4, export_path) + except TankError, e: + errors.append("%s" % e) + return errors + + p4_fw.util.add_to_change(p4, change, export_path) + + # store additional metadata for the publish: + progress_cb(85, "Storing publish data") + publish_data = {"thumbnail_path": thumbnail_path, + "dependency_paths": [primary_publish_path], + "task": sg_task, + "comment": comment, + "context": self.parent.context, + "published_file_type": publish_type + } + + try: + p4_fw.store_publish_data(export_path, publish_data) + except TankError, e: + errors.append("Failed to store publish data: %s" % e) + + finally: + # delete the thumbnail file: + if os.path.exists(thumbnail_path): + try: + os.remove(thumbnail_path) + except: + pass + + # set units back to original + adobe.app.preferences.rulerUnits = original_ruler_units + + return errors + + def __send_to_review(self, primary_publish_path, sg_task, comment, p4, p4_fw, progress_cb): + """ + Create a version of the current document that can be uploaded as a + Shotgun 'Version' entity and reviewed in Screening Room, etc. + """ + adobe = self.parent.engine.adobe + errors = [] + + progress_cb(10, "Saving JPEG version of file") + + # set up the export options and get a file object: + jpeg_path = os.path.join(tempfile.gettempdir(), "%s_sgtk.jpg" % uuid.uuid4().hex) + jpeg_file = adobe.RemoteObject('flash.filesystem::File', jpeg_path) + jpeg_save_options = adobe.RemoteObject('com.adobe.photoshop::JPEGSaveOptions') + jpeg_save_options.quality = 12 + + try: + + # save as a copy: + adobe.app.activeDocument.saveAs(jpeg_file, jpeg_save_options, True) + + # construct the data needed to create a Shotgun 'Version' entity: + ctx = self.parent.context + data = { + "code": os.path.basename(primary_publish_path), + "sg_first_frame": 1, + "frame_count": 1, + "frame_range": "1-1", + "sg_last_frame": 1, + "entity": ctx.entity, + "sg_path_to_frames": primary_publish_path, + "project": ctx.project, + "sg_task": sg_task, + "sg_uploaded_movie": jpeg_path + } + + # and store the version data for the publish path: + progress_cb(50.0, "Storing review data...") + try: + p4_fw.store_publish_review_data(primary_publish_path, data) + except TankError, e: + errors.append("Failed to store review data: %s" % e) + + finally: + # delete the temp jpeg file: + if os.path.exists(jpeg_path): + try: + os.remove(jpeg_path) + except: + pass + + return errors diff --git a/hooks/tk-multi-workfiles/scene_operation_tk-photoshopcc.py b/hooks/tk-multi-workfiles/scene_operation_tk-photoshopcc.py new file mode 100644 index 0000000..6522253 --- /dev/null +++ b/hooks/tk-multi-workfiles/scene_operation_tk-photoshopcc.py @@ -0,0 +1,132 @@ +# Copyright (c) 2013 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +# import os +# import photoshop + +from tank import Hook +from tank import TankError + +TK_FRAMEWORK_PERFORCE_NAME = "tk-framework-perforce_v0.x.x" + + +class SceneOperation(Hook): + """ + Hook called to perform an operation with the + current scene + """ + + def execute(self, operation, file_path, context, parent_action, file_version, read_only, **kwargs): + """ + Main hook entry point + + :operation: String + Scene operation to perform + + :file_path: String + File path to use if the operation + requires it (e.g. open) + + :context: Context + The context the file operation is being + performed in. + + :parent_action: This is the action that this scene operation is + being executed for. This can be one of: + - open_file + - new_file + - save_file_as + - version_up + + :file_version: The version/revision of the file to be opened. If this is 'None' + then the latest version should be opened. + + :read_only: Specifies if the file should be opened read-only or not + + :returns: Depends on operation: + 'current_path' - Return the current scene + file path as a String + 'reset' - True if scene was reset to an empty + state, otherwise False + all others - None + """ + p4_fw = self.load_framework(TK_FRAMEWORK_PERFORCE_NAME) + adobe = self.parent.engine.adobe + + if operation == "current_path": + # return the current script path + doc = self._get_active_document() + + if doc.fullName is None: + # new file? + path = "" + else: + path = doc.fullName.nativePath + + return path + + elif operation == "open": + # check that we have the correct version synced: + p4 = p4_fw.connection.connect() + if read_only: + pass + # just sync the file: + # (TODO) - move this to the framework + # path_to_sync = file_path + # if file_version: + # # sync specific version: + # path_to_sync = "%s#%s" % (path_to_sync, file_version) + # try: + # p4.run_sync(path_to_sync) + # except P4Exception, e: + # raise TankError("Failed to sync file '%s'" % path_to_sync) + else: + # open the file for edit: + # p4_fw.util.open_file_for_edit(p4, file_path, add_if_new=False, version=file_version) + p4_fw.util.open_file_for_edit(p4, file_path, add_if_new=False) + + # open the file + f = adobe.File(file_path) + adobe.app.load(f) + + elif operation == "save": + # save the current script: + doc = self._get_active_document() + doc.save() + + elif operation == "save_as": + doc = self._get_active_document() + + # and check out the file for edit: + p4 = p4_fw.connection.connect() + p4_fw.util.open_file_for_edit(p4, file_path, add_if_new=False) + + adobe.save_as(doc, file_path) + + elif operation == "reset": + # do nothing and indicate scene was reset to empty + return True + + elif operation == "prepare_new": + # file->new. Not sure how to pop up the actual file->new UI, + # this command will create a document with default properties + adobe.app.documents.add() + + def _get_active_document(self): + """ + Returns the currently open document in Photoshop. + Raises an exeption if no document is active. + """ + try: + doc = self.parent.engine.adobe.app.activeDocument + except RuntimeError: + raise TankError("There is no active document!") + + return doc