diff --git a/pype/hosts/photoshop/__init__.py b/pype/hosts/photoshop/__init__.py new file mode 100644 index 00000000000..01ed757a8df --- /dev/null +++ b/pype/hosts/photoshop/__init__.py @@ -0,0 +1,33 @@ +import os + +from avalon import api +import pyblish.api + + +def install(): + print("Installing Pype config...") + + plugins_directory = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "plugins", + "photoshop" + ) + + pyblish.api.register_plugin_path( + os.path.join(plugins_directory, "publish") + ) + api.register_plugin_path( + api.Loader, os.path.join(plugins_directory, "load") + ) + api.register_plugin_path( + api.Creator, os.path.join(plugins_directory, "create") + ) + + pyblish.api.register_callback( + "instanceToggled", on_pyblish_instance_toggled + ) + + +def on_pyblish_instance_toggled(instance, old_value, new_value): + """Toggle layer visibility on instance toggles.""" + instance[0].Visible = new_value diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index adff8aa3fa4..f8429e8b589 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -77,7 +77,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "gizmo", "source", "matchmove", - "image" + "image", "source", "assembly", "fbx", diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py new file mode 100644 index 00000000000..a840dd13a79 --- /dev/null +++ b/pype/plugins/photoshop/create/create_image.py @@ -0,0 +1,12 @@ +from avalon import photoshop + + +class CreateImage(photoshop.Creator): + """Image folder for publish.""" + + name = "imageDefault" + label = "Image" + family = "image" + + def __init__(self, *args, **kwargs): + super(CreateImage, self).__init__(*args, **kwargs) diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py new file mode 100644 index 00000000000..18efe750d59 --- /dev/null +++ b/pype/plugins/photoshop/load/load_image.py @@ -0,0 +1,43 @@ +from avalon import api, photoshop + + +class ImageLoader(api.Loader): + """Load images + + Stores the imported asset in a container named after the asset. + """ + + families = ["image"] + representations = ["*"] + + def load(self, context, name=None, namespace=None, data=None): + with photoshop.maintained_selection(): + layer = photoshop.import_smart_object(self.fname) + + self[:] = [layer] + + return photoshop.containerise( + name, + namespace, + layer, + context, + self.__class__.__name__ + ) + + def update(self, container, representation): + layer = container.pop("layer") + + with photoshop.maintained_selection(): + photoshop.replace_smart_object( + layer, api.get_representation_path(representation) + ) + + photoshop.imprint( + layer, {"representation": str(representation["_id"])} + ) + + def remove(self, container): + container["layer"].Delete() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py new file mode 100644 index 00000000000..4308588559d --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -0,0 +1,17 @@ +import os + +import pyblish.api +from avalon import photoshop + + +class CollectCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder - 0.5 + label = "Current File" + hosts = ["photoshop"] + + def process(self, context): + context.data["currentFile"] = os.path.normpath( + photoshop.app().ActiveDocument.FullName + ).replace("\\", "/") diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py new file mode 100644 index 00000000000..4937f2a1e42 --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -0,0 +1,56 @@ +import pythoncom + +from avalon import photoshop + +import pyblish.api + + +class CollectInstances(pyblish.api.ContextPlugin): + """Gather instances by LayerSet and file metadata + + This collector takes into account assets that are associated with + an LayerSet and marked with a unique identifier; + + Identifier: + id (str): "pyblish.avalon.instance" + """ + + label = "Instances" + order = pyblish.api.CollectorOrder + hosts = ["photoshop"] + families_mapping = { + "image": [] + } + + def process(self, context): + # Necessary call when running in a different thread which pyblish-qml + # can be. + pythoncom.CoInitialize() + + for layer in photoshop.get_layers_in_document(): + layer_data = photoshop.read(layer) + + # Skip layers without metadata. + if layer_data is None: + continue + + # Skip containers. + if "container" in layer_data["id"]: + continue + + child_layers = [*layer.Layers] + if not child_layers: + self.log.info("%s skipped, it was empty." % layer.Name) + continue + + instance = context.create_instance(layer.Name) + instance.append(layer) + instance.data.update(layer_data) + instance.data["families"] = self.families_mapping[ + layer_data["family"] + ] + instance.data["publish"] = layer.Visible + + # Produce diagnostic message for any graphical + # user interface interested in visualising it. + self.log.info("Found: \"%s\" " % instance.data["name"]) diff --git a/pype/plugins/photoshop/publish/collect_workfile.py b/pype/plugins/photoshop/publish/collect_workfile.py new file mode 100644 index 00000000000..766be02354a --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_workfile.py @@ -0,0 +1,39 @@ +import pyblish.api +import os + + +class CollectWorkfile(pyblish.api.ContextPlugin): + """Collect current script for publish.""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect Workfile" + hosts = ["photoshop"] + + def process(self, context): + family = "workfile" + task = os.getenv("AVALON_TASK", None) + subset = family + task.capitalize() + + file_path = context.data["currentFile"] + staging_dir = os.path.dirname(file_path) + base_name = os.path.basename(file_path) + + # Create instance + instance = context.create_instance(subset) + instance.data.update({ + "subset": subset, + "label": base_name, + "name": base_name, + "family": family, + "families": [], + "representations": [], + "asset": os.environ["AVALON_ASSET"] + }) + + # creating representation + instance.data["representations"].append({ + "name": "psd", + "ext": "psd", + "files": base_name, + "stagingDir": staging_dir, + }) diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py new file mode 100644 index 00000000000..da3197c7daf --- /dev/null +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -0,0 +1,62 @@ +import os + +import pype.api +from avalon import photoshop + + +class ExtractImage(pype.api.Extractor): + """Produce a flattened image file from instance + + This plug-in takes into account only the layers in the group. + """ + + label = "Extract Image" + hosts = ["photoshop"] + families = ["image"] + + def process(self, instance): + + staging_dir = self.staging_dir(instance) + self.log.info("Outputting image to {}".format(staging_dir)) + + # Perform extraction + files = {} + with photoshop.maintained_selection(): + self.log.info("Extracting %s" % str(list(instance))) + with photoshop.maintained_visibility(): + # Hide all other layers. + extract_ids = [ + x.id for x in photoshop.get_layers_in_layers([instance[0]]) + ] + for layer in photoshop.get_layers_in_document(): + if layer.id not in extract_ids: + layer.Visible = False + + save_options = { + "png": photoshop.com_objects.PNGSaveOptions(), + "jpg": photoshop.com_objects.JPEGSaveOptions() + } + + for extension, save_option in save_options.items(): + photoshop.app().ActiveDocument.SaveAs( + staging_dir, save_option, True + ) + files[extension] = "{} copy.{}".format( + os.path.splitext( + photoshop.app().ActiveDocument.Name + )[0], + extension + ) + + representations = [] + for extension, filename in files.items(): + representations.append({ + "name": extension, + "ext": extension, + "files": filename, + "stagingDir": staging_dir + }) + instance.data["representations"] = representations + instance.data["stagingDir"] = staging_dir + + self.log.info(f"Extracted {instance} to {staging_dir}") diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py new file mode 100644 index 00000000000..b3d4f0e447b --- /dev/null +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -0,0 +1,14 @@ +import pype.api +from avalon import photoshop + + +class ExtractSaveScene(pype.api.Extractor): + """Save scene before extraction.""" + + order = pype.api.Extractor.order - 0.49 + label = "Extract Save Scene" + hosts = ["photoshop"] + families = ["workfile"] + + def process(self, instance): + photoshop.app().ActiveDocument.Save() diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py new file mode 100644 index 00000000000..ab1d02269f8 --- /dev/null +++ b/pype/plugins/photoshop/publish/validate_instance_asset.py @@ -0,0 +1,48 @@ +import os + +import pyblish.api +import pype.api +from avalon import photoshop + + +class ValidateInstanceAssetRepair(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + for instance in instances: + data = photoshop.read(instance[0]) + data["asset"] = os.environ["AVALON_ASSET"] + photoshop.imprint(instance[0], data) + + +class ValidateInstanceAsset(pyblish.api.InstancePlugin): + """Validate the instance asset is the current asset.""" + + label = "Validate Instance Asset" + hosts = ["photoshop"] + actions = [ValidateInstanceAssetRepair] + order = pype.api.ValidateContentsOrder + + def process(self, instance): + instance_asset = instance.data["asset"] + current_asset = os.environ["AVALON_ASSET"] + msg = ( + "Instance asset is not the same as current asset:" + f"\nInstance: {instance_asset}\nCurrent: {current_asset}" + ) + assert instance_asset == current_asset, msg diff --git a/res/app_icons/photoshop.png b/res/app_icons/photoshop.png new file mode 100644 index 00000000000..c7e9d147112 Binary files /dev/null and b/res/app_icons/photoshop.png differ