diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index 94152b57065..c5a12cba06d 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -7,28 +7,15 @@ from .launch_logic import stub from .pipeline import ( + PhotoshopHost, ls, - list_instances, - remove_instance, - install, - uninstall, - containerise, - get_context_data, - update_context_data, - get_context_title + containerise ) from .plugin import ( PhotoshopLoader, get_unique_layer_name ) -from .workio import ( - file_extensions, - has_unsaved_changes, - save_file, - open_file, - current_file, - work_root, -) + from .lib import ( maintained_selection, @@ -40,28 +27,14 @@ "stub", # pipeline + "PhotoshopHost", "ls", - "list_instances", - "remove_instance", - "install", - "uninstall", "containerise", - "get_context_data", - "update_context_data", - "get_context_title", # Plugin "PhotoshopLoader", "get_unique_layer_name", - # workfiles - "file_extensions", - "has_unsaved_changes", - "save_file", - "open_file", - "current_file", - "work_root", - # lib "maintained_selection", "maintained_visibility", diff --git a/openpype/hosts/photoshop/api/extension.zxp b/openpype/hosts/photoshop/api/extension.zxp index a25ec96e7df..39b766cd0d3 100644 Binary files a/openpype/hosts/photoshop/api/extension.zxp and b/openpype/hosts/photoshop/api/extension.zxp differ diff --git a/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml b/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml index 6396cd24122..2089d06da19 100644 --- a/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml +++ b/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml @@ -1,5 +1,5 @@ - + diff --git a/openpype/hosts/photoshop/api/extension/extension.zxp b/openpype/hosts/photoshop/api/extension/extension.zxp new file mode 100644 index 00000000000..39b766cd0d3 Binary files /dev/null and b/openpype/hosts/photoshop/api/extension/extension.zxp differ diff --git a/openpype/hosts/photoshop/api/extension/index.html b/openpype/hosts/photoshop/api/extension/index.html index 501e753c0b4..9d7363e62d9 100644 --- a/openpype/hosts/photoshop/api/extension/index.html +++ b/openpype/hosts/photoshop/api/extension/index.html @@ -32,17 +32,6 @@ }); - - - - - - diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py index 1f0203dca6d..97a2dd94f67 100644 --- a/openpype/hosts/photoshop/api/launch_logic.py +++ b/openpype/hosts/photoshop/api/launch_logic.py @@ -334,9 +334,6 @@ async def read(self): return await self.socket.call('photoshop.read') # panel routes for tools - async def creator_route(self): - self._tool_route("creator") - async def workfiles_route(self): self._tool_route("workfiles") @@ -344,14 +341,11 @@ async def loader_route(self): self._tool_route("loader") async def publish_route(self): - self._tool_route("publish") + self._tool_route("publisher") async def sceneinventory_route(self): self._tool_route("sceneinventory") - async def subsetmanager_route(self): - self._tool_route("subsetmanager") - async def experimental_tools_route(self): self._tool_route("experimental_tools") diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index 221b4314e6f..a16e2c43ca7 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -20,9 +20,11 @@ def safe_excepthook(*args): def main(*subprocess_args): - from openpype.hosts.photoshop import api + from openpype.hosts.photoshop.api import PhotoshopHost + + host = PhotoshopHost() + install_host(host) - install_host(api) sys.excepthook = safe_excepthook # coloring in StdOutBroker diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 9f6fc0983ce..5a23a2213ca 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -1,6 +1,5 @@ import os from Qt import QtWidgets - import pyblish.api from openpype.lib import register_event_callback, Logger @@ -12,6 +11,14 @@ deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) + +from openpype.host import ( + HostBase, + IWorkfileHost, + ILoadHost, + IPublishHost +) + from openpype.pipeline.load import any_outdated_containers from openpype.hosts.photoshop import PHOTOSHOP_HOST_DIR @@ -26,6 +33,140 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") +class PhotoshopHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): + name = "photoshop" + + def install(self): + """Install Photoshop-specific functionality needed for integration. + + This function is called automatically on calling + `api.install(photoshop)`. + """ + log.info("Installing OpenPype Photoshop...") + pyblish.api.register_host("photoshop") + + pyblish.api.register_plugin_path(PUBLISH_PATH) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + log.info(PUBLISH_PATH) + + pyblish.api.register_callback( + "instanceToggled", on_pyblish_instance_toggled + ) + + register_event_callback("application.launched", on_application_launch) + + def current_file(self): + try: + full_name = lib.stub().get_active_document_full_name() + if full_name and full_name != "null": + return os.path.normpath(full_name).replace("\\", "/") + except Exception: + pass + + return None + + def work_root(self, session): + return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/") + + def open_workfile(self, filepath): + lib.stub().open(filepath) + + return True + + def save_workfile(self, filepath=None): + _, ext = os.path.splitext(filepath) + lib.stub().saveAs(filepath, ext[1:], True) + + def get_current_workfile(self): + return self.current_file() + + def workfile_has_unsaved_changes(self): + if self.current_file(): + return not lib.stub().is_saved() + + return False + + def get_workfile_extensions(self): + return [".psd", ".psb"] + + def get_containers(self): + return ls() + + def get_context_data(self): + """Get stored values for context (validation enable/disable etc)""" + meta = _get_stub().get_layers_metadata() + for item in meta: + if item.get("id") == "publish_context": + item.pop("id") + return item + + return {} + + def update_context_data(self, data, changes): + """Store value needed for context""" + item = data + item["id"] = "publish_context" + _get_stub().imprint(item["id"], item) + + def get_context_title(self): + """Returns title for Creator window""" + + project_name = legacy_io.Session["AVALON_PROJECT"] + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + return "{}/{}/{}".format(project_name, asset_name, task_name) + + def list_instances(self): + """List all created instances to publish from current workfile. + + Pulls from File > File Info + + Returns: + (list) of dictionaries matching instances format + """ + stub = _get_stub() + + if not stub: + return [] + + instances = [] + layers_meta = stub.get_layers_metadata() + if layers_meta: + for instance in layers_meta: + if instance.get("id") == "pyblish.avalon.instance": + instances.append(instance) + + return instances + + def remove_instance(self, instance): + """Remove instance from current workfile metadata. + + Updates metadata of current file in File > File Info and removes + icon highlight on group layer. + + Args: + instance (dict): instance representation from subsetmanager model + """ + stub = _get_stub() + + if not stub: + return + + inst_id = instance.get("instance_id") or instance.get("uuid") # legacy + if not inst_id: + log.warning("No instance identifier for {}".format(instance)) + return + + stub.remove_instance(inst_id) + + if instance.get("members"): + item = stub.get_layer(instance["members"][0]) + if item: + stub.rename_layer(item.id, + item.name.replace(stub.PUBLISH_ICON, '')) + + def check_inventory(): if not any_outdated_containers(): return @@ -52,32 +193,6 @@ def on_pyblish_instance_toggled(instance, old_value, new_value): instance[0].Visible = new_value -def install(): - """Install Photoshop-specific functionality of avalon-core. - - This function is called automatically on calling `api.install(photoshop)`. - """ - log.info("Installing OpenPype Photoshop...") - pyblish.api.register_host("photoshop") - - pyblish.api.register_plugin_path(PUBLISH_PATH) - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) - log.info(PUBLISH_PATH) - - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) - - register_event_callback("application.launched", on_application_launch) - - -def uninstall(): - pyblish.api.deregister_plugin_path(PUBLISH_PATH) - deregister_loader_plugin_path(LOAD_PATH) - deregister_creator_plugin_path(CREATE_PATH) - - def ls(): """Yields containers from active Photoshop document @@ -117,61 +232,6 @@ def ls(): yield data -def list_instances(): - """List all created instances to publish from current workfile. - - Pulls from File > File Info - - For SubsetManager - - Returns: - (list) of dictionaries matching instances format - """ - stub = _get_stub() - - if not stub: - return [] - - instances = [] - layers_meta = stub.get_layers_metadata() - if layers_meta: - for instance in layers_meta: - if instance.get("id") == "pyblish.avalon.instance": - instances.append(instance) - - return instances - - -def remove_instance(instance): - """Remove instance from current workfile metadata. - - Updates metadata of current file in File > File Info and removes - icon highlight on group layer. - - For SubsetManager - - Args: - instance (dict): instance representation from subsetmanager model - """ - stub = _get_stub() - - if not stub: - return - - inst_id = instance.get("instance_id") or instance.get("uuid") # legacy - if not inst_id: - log.warning("No instance identifier for {}".format(instance)) - return - - stub.remove_instance(inst_id) - - if instance.get("members"): - item = stub.get_layer(instance["members"][0]) - if item: - stub.rename_layer(item.id, - item.name.replace(stub.PUBLISH_ICON, '')) - - def _get_stub(): """Handle pulling stub from PS to run operations on host @@ -226,28 +286,17 @@ def containerise( return layer -def get_context_data(): - """Get stored values for context (validation enable/disable etc)""" - meta = _get_stub().get_layers_metadata() - for item in meta: - if item.get("id") == "publish_context": - item.pop("id") - return item - - return {} - - -def update_context_data(data, changes): - """Store value needed for context""" - item = data - item["id"] = "publish_context" - _get_stub().imprint(item["id"], item) +def cache_and_get_instances(creator): + """Cache instances in shared data. - -def get_context_title(): - """Returns title for Creator window""" - - project_name = legacy_io.Session["AVALON_PROJECT"] - asset_name = legacy_io.Session["AVALON_ASSET"] - task_name = legacy_io.Session["AVALON_TASK"] - return "{}/{}/{}".format(project_name, asset_name, task_name) + Storing all instances as a list as legacy instances might be still present. + Args: + creator (Creator): Plugin which would like to get instances from host. + Returns: + List[]: list of all instances stored in metadata + """ + shared_key = "openpype.photoshop.instances" + if shared_key not in creator.collection_shared_data: + creator.collection_shared_data[shared_key] = \ + creator.host.list_instances() + return creator.collection_shared_data[shared_key] diff --git a/openpype/hosts/photoshop/api/workio.py b/openpype/hosts/photoshop/api/workio.py deleted file mode 100644 index 35b44d60708..00000000000 --- a/openpype/hosts/photoshop/api/workio.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Host API required Work Files tool""" -import os - -from . import lib - - -def _active_document(): - document_name = lib.stub().get_active_document_name() - if not document_name: - return None - - return document_name - - -def file_extensions(): - return [".psd", ".psb"] - - -def has_unsaved_changes(): - if _active_document(): - return not lib.stub().is_saved() - - return False - - -def save_file(filepath): - _, ext = os.path.splitext(filepath) - lib.stub().saveAs(filepath, ext[1:], True) - - -def open_file(filepath): - lib.stub().open(filepath) - - return True - - -def current_file(): - try: - full_name = lib.stub().get_active_document_full_name() - if full_name and full_name != "null": - return os.path.normpath(full_name).replace("\\", "/") - except Exception: - pass - - return None - - -def work_root(session): - return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/") diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 2cfbfa8778c..ca3bbfd27c0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -9,6 +9,7 @@ ) from openpype.lib import prepare_template_data from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS +from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances class ImageCreator(Creator): @@ -19,7 +20,7 @@ class ImageCreator(Creator): description = "Image creator" def collect_instances(self): - for instance_data in api.list_instances(): + for instance_data in cache_and_get_instances(self): # legacy instances have family=='image' creator_id = (instance_data.get("creator_identifier") or instance_data.get("family")) @@ -97,6 +98,7 @@ def create(self, subset_name_from_ui, data, pre_create_data): data.update({"subset": subset_name}) data.update({"members": [str(group.id)]}) + data.update({"layer_name": layer_name}) data.update({"long_name": "_".join(layer_names_in_hierarchy)}) new_instance = CreatedInstance(self.family, subset_name, data, @@ -121,7 +123,7 @@ def update_instances(self, update_list): def remove_instances(self, instances): for instance in instances: - api.remove_instance(instance) + self.host.remove_instance(instance) self._remove_instance_from_context(instance) def get_default_variants(self): @@ -163,6 +165,11 @@ def _handle_legacy(self, instance_data): def _clean_highlights(self, stub, item): return item.replace(stub.PUBLISH_ICON, '').replace(stub.LOADED_ICON, '') - @classmethod - def get_dynamic_data(cls, *args, **kwargs): + + def get_dynamic_data(self, variant, task_name, asset_doc, + project_name, host_name, instance): + if instance is not None: + layer_name = instance.get("layer_name") + if layer_name: + return {"layer": layer_name} return {"layer": "{layer}"} diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py deleted file mode 100644 index 2792a775e06..00000000000 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ /dev/null @@ -1,119 +0,0 @@ -import re - -from Qt import QtWidgets -from openpype.pipeline import create -from openpype.hosts.photoshop import api as photoshop - -from openpype.lib import prepare_template_data -from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS - - -class CreateImage(create.LegacyCreator): - """Image folder for publish.""" - - name = "imageDefault" - label = "Image" - family = "image" - defaults = ["Main"] - - def process(self): - groups = [] - layers = [] - create_group = False - - stub = photoshop.stub() - if (self.options or {}).get("useSelection"): - multiple_instances = False - selection = stub.get_selected_layers() - self.log.info("selection {}".format(selection)) - if len(selection) > 1: - # Ask user whether to create one image or image per selected - # item. - msg_box = QtWidgets.QMessageBox() - msg_box.setIcon(QtWidgets.QMessageBox.Warning) - msg_box.setText( - "Multiple layers selected." - "\nDo you want to make one image per layer?" - ) - msg_box.setStandardButtons( - QtWidgets.QMessageBox.Yes | - QtWidgets.QMessageBox.No | - QtWidgets.QMessageBox.Cancel - ) - ret = msg_box.exec_() - if ret == QtWidgets.QMessageBox.Yes: - multiple_instances = True - elif ret == QtWidgets.QMessageBox.Cancel: - return - - if multiple_instances: - for item in selection: - if item.group: - groups.append(item) - else: - layers.append(item) - else: - group = stub.group_selected_layers(self.name) - groups.append(group) - - elif len(selection) == 1: - # One selected item. Use group if its a LayerSet (group), else - # create a new group. - if selection[0].group: - groups.append(selection[0]) - else: - layers.append(selection[0]) - elif len(selection) == 0: - # No selection creates an empty group. - create_group = True - else: - group = stub.create_group(self.name) - groups.append(group) - - if create_group: - group = stub.create_group(self.name) - groups.append(group) - - for layer in layers: - stub.select_layers([layer]) - group = stub.group_selected_layers(layer.name) - groups.append(group) - - creator_subset_name = self.data["subset"] - layer_name = '' - for group in groups: - long_names = [] - group.name = group.name.replace(stub.PUBLISH_ICON, ''). \ - replace(stub.LOADED_ICON, '') - - subset_name = creator_subset_name - if len(groups) > 1: - layer_name = re.sub( - "[^{}]+".format(SUBSET_NAME_ALLOWED_SYMBOLS), - "", - group.name - ) - if "{layer}" not in subset_name.lower(): - subset_name += "{Layer}" - - layer_fill = prepare_template_data({"layer": layer_name}) - subset_name = subset_name.format(**layer_fill) - - if group.long_name: - for directory in group.long_name[::-1]: - name = directory.replace(stub.PUBLISH_ICON, '').\ - replace(stub.LOADED_ICON, '') - long_names.append(name) - - self.data.update({"subset": subset_name}) - self.data.update({"uuid": str(group.id)}) - self.data.update({"members": [str(group.id)]}) - self.data.update({"long_name": "_".join(long_names)}) - stub.imprint(group, self.data) - # reusing existing group, need to rename afterwards - if not create_group: - stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) - - @classmethod - def get_dynamic_data(cls, *args, **kwargs): - return {"layer": "{layer}"} diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/plugins/create/workfile_creator.py index e79d16d154f..8ee9a0d8322 100644 --- a/openpype/hosts/photoshop/plugins/create/workfile_creator.py +++ b/openpype/hosts/photoshop/plugins/create/workfile_creator.py @@ -5,6 +5,7 @@ CreatedInstance, legacy_io ) +from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances class PSWorkfileCreator(AutoCreator): @@ -17,7 +18,7 @@ def get_instance_attr_defs(self): return [] def collect_instances(self): - for instance_data in api.list_instances(): + for instance_data in cache_and_get_instances(self): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: subset_name = instance_data["subset"] @@ -54,7 +55,7 @@ def create(self, options=None): } data.update(self.get_dynamic_data( self.default_variant, task_name, asset_doc, - project_name, host_name + project_name, host_name, None )) new_instance = CreatedInstance( diff --git a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py index 64c99b4fc1e..dc0678c9af6 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py @@ -43,7 +43,7 @@ def process(self, context): with open(manifest_url) as fp: content = fp.read() - found = re.findall(r'(ExtensionBundleVersion=")([0-10\.]+)(")', + found = re.findall(r'(ExtensionBundleVersion=")([0-9\.]+)(")', content) if found: expected_version = found[0][1] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index b466ec86879..5bf12379b17 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -82,7 +82,7 @@ def process(self, context): if len(instance_names) != len(set(instance_names)): self.log.warning("Duplicate instances found. " + - "Remove unwanted via SubsetManager") + "Remove unwanted via Publisher") if len(instance_names) == 0 and self.flatten_subset_template: project_name = context.data["projectEntity"]["name"] diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml new file mode 100644 index 00000000000..e05ac921827 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml @@ -0,0 +1,20 @@ + + + +Asset does not match + +## Collected asset name is not same as in context + + {msg} +### How to repair? + {repair_msg} + Refresh Publish afterwards (circle arrow at the bottom right). + + If that's not correct value, close workfile and reopen via Workfiles to get + proper context asset name OR disable this validator and publish again + if you are publishing to different context deliberately. + + (Context means combination of project, asset name and task name.) + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml index 5a1e266748b..023bbf26fa7 100644 --- a/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml @@ -10,7 +10,7 @@ Subset or layer name cannot contain specific characters (spaces etc) which could ### How to repair? -You can fix this with "repair" button on the right. +You can fix this with "repair" button on the right and press Refresh publishing button at the bottom right. ### __Detailed Info__ (optional) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py index 2609f7a8cf8..b9d721dbdb4 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -1,7 +1,11 @@ import pyblish.api from openpype.pipeline import legacy_io -from openpype.pipeline.publish import ValidateContentsOrder +from openpype.pipeline.publish import ( + ValidateContentsOrder, + PublishXmlValidationError, + OptionalPyblishPluginMixin +) from openpype.hosts.photoshop import api as photoshop @@ -31,30 +35,38 @@ def process(self, context, plugin): stub.imprint(instance[0], data) -class ValidateInstanceAsset(pyblish.api.InstancePlugin): +class ValidateInstanceAsset(OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin): """Validate the instance asset is the current selected context asset. - As it might happen that multiple worfiles are opened, switching - between them would mess with selected context. - In that case outputs might be output under wrong asset! + As it might happen that multiple worfiles are opened, switching + between them would mess with selected context. + In that case outputs might be output under wrong asset! - Repair action will use Context asset value (from Workfiles or Launcher) - Closing and reopening with Workfiles will refresh Context value. + Repair action will use Context asset value (from Workfiles or Launcher) + Closing and reopening with Workfiles will refresh Context value. """ label = "Validate Instance Asset" hosts = ["photoshop"] + optional = True actions = [ValidateInstanceAssetRepair] order = ValidateContentsOrder def process(self, instance): instance_asset = instance.data["asset"] current_asset = legacy_io.Session["AVALON_ASSET"] - msg = ( - f"Instance asset {instance_asset} is not the same " - f"as current context {current_asset}. PLEASE DO:\n" - f"Repair with 'A' action to use '{current_asset}'.\n" - f"If that's not correct value, close workfile and " - f"reopen via Workfiles!" - ) - assert instance_asset == current_asset, msg + + if instance_asset != current_asset: + msg = ( + f"Instance asset {instance_asset} is not the same " + f"as current context {current_asset}." + + ) + repair_msg = ( + f"Repair with 'Repair' button to use '{current_asset}'.\n" + ) + formatting_data = {"msg": msg, + "repair_msg": repair_msg} + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index 0665aff9d06..07810f505ec 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -84,7 +84,7 @@ class ValidateNaming(pyblish.api.InstancePlugin): replace_char = '' def process(self, instance): - help_msg = ' Use Repair action (A) in Pyblish to fix it.' + help_msg = ' Use Repair button to fix it and then refresh publish.' layer = instance.data.get("layer") if layer: diff --git a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py index 78e84729cea..09859ca7c6f 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py @@ -29,7 +29,7 @@ def process(self, context): for item, count in collections.Counter(subset_names).items() if count > 1] msg = ("Instance subset names {} are not unique. ".format(non_unique) + - "Remove duplicates via SubsetManager.") + "Remove duplicates via Publisher.") formatting_data = { "non_unique": ",".join(non_unique) } diff --git a/openpype/hosts/traypublisher/api/plugin.py b/openpype/hosts/traypublisher/api/plugin.py index 555041d3897..6b95379cf24 100644 --- a/openpype/hosts/traypublisher/api/plugin.py +++ b/openpype/hosts/traypublisher/api/plugin.py @@ -1,49 +1,32 @@ from openpype.lib.attribute_definitions import FileDef +from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS from openpype.pipeline.create import ( Creator, HiddenCreator, - CreatedInstance + CreatedInstance, + cache_and_get_instances, ) - from .pipeline import ( list_instances, update_instances, remove_instances, HostContext, ) -from openpype.lib.transcoding import IMAGE_EXTENSIONS, VIDEO_EXTENSIONS - - -REVIEW_EXTENSIONS = IMAGE_EXTENSIONS + VIDEO_EXTENSIONS - -def _cache_and_get_instances(creator): - """Cache instances in shared data. - - Args: - creator (Creator): Plugin which would like to get instances from host. - - Returns: - List[Dict[str, Any]]: Cached instances list from host implementation. - """ - - shared_key = "openpype.traypublisher.instances" - if shared_key not in creator.collection_shared_data: - creator.collection_shared_data[shared_key] = list_instances() - return creator.collection_shared_data[shared_key] +REVIEW_EXTENSIONS = set(IMAGE_EXTENSIONS) | set(VIDEO_EXTENSIONS) +SHARED_DATA_KEY = "openpype.traypublisher.instances" class HiddenTrayPublishCreator(HiddenCreator): host_name = "traypublisher" def collect_instances(self): - for instance_data in _cache_and_get_instances(self): - creator_id = instance_data.get("creator_identifier") - if creator_id == self.identifier: - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) + instances_by_identifier = cache_and_get_instances( + self, SHARED_DATA_KEY, list_instances + ) + for instance_data in instances_by_identifier[self.identifier]: + instance = CreatedInstance.from_existing(instance_data, self) + self._add_instance_to_context(instance) def update_instances(self, update_list): update_instances(update_list) @@ -74,13 +57,12 @@ class TrayPublishCreator(Creator): host_name = "traypublisher" def collect_instances(self): - for instance_data in _cache_and_get_instances(self): - creator_id = instance_data.get("creator_identifier") - if creator_id == self.identifier: - instance = CreatedInstance.from_existing( - instance_data, self - ) - self._add_instance_to_context(instance) + instances_by_identifier = cache_and_get_instances( + self, SHARED_DATA_KEY, list_instances + ) + for instance_data in instances_by_identifier[self.identifier]: + instance = CreatedInstance.from_existing(instance_data, self) + self._add_instance_to_context(instance) def update_instances(self, update_list): update_instances(update_list) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index 61b95cf06da..9b35c9502dc 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -7,7 +7,12 @@ import platform import uuid import re -from Deadline.Scripting import RepositoryUtils, FileUtils, DirectoryUtils +from Deadline.Scripting import ( + RepositoryUtils, + FileUtils, + DirectoryUtils, + ProcessUtils, +) def get_openpype_version_from_path(path, build=True): @@ -162,9 +167,8 @@ def inject_openpype_environment(deadlinePlugin): print(">>> Temporary path: {}".format(export_url)) args = [ - exe, "--headless", - 'extractenvironments', + "extractenvironments", export_url ] @@ -188,15 +192,18 @@ def inject_openpype_environment(deadlinePlugin): if not os.environ.get("OPENPYPE_MONGO"): print(">>> Missing OPENPYPE_MONGO env var, process won't work") - env = os.environ - env["OPENPYPE_HEADLESS_MODE"] = "1" - env["AVALON_TIMEOUT"] = "5000" + os.environ["AVALON_TIMEOUT"] = "5000" - print(">>> Executing: {}".format(" ".join(args))) - std_output = subprocess.check_output(args, - cwd=os.path.dirname(exe), - env=env) - print(">>> Process result {}".format(std_output)) + args_str = subprocess.list2cmdline(args) + print(">>> Executing: {} {}".format(exe, args_str)) + process = ProcessUtils.SpawnProcess( + exe, args_str, os.path.dirname(exe) + ) + ProcessUtils.WaitForExit(process, -1) + if process.ExitCode != 0: + raise RuntimeError( + "Failed to run OpenPype process to extract environments." + ) print(">>> Loading file ...") with open(export_url) as fp: diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 4b91951a088..9e858151fdf 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -24,6 +24,8 @@ deregister_creator_plugin, register_creator_plugin_path, deregister_creator_plugin_path, + + cache_and_get_instances, ) from .context import ( diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index c69abb88612..0f9c346966b 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -1,5 +1,6 @@ import os import copy +import collections from abc import ( ABCMeta, @@ -660,3 +661,34 @@ def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) deregister_plugin_path(SubsetConvertorPlugin, path) + + +def cache_and_get_instances(creator, shared_key, list_instances_func): + """Common approach to cache instances in shared data. + + This is helper function which does not handle cases when a 'shared_key' is + used for different list instances functions. The same approach of caching + instances into 'collection_shared_data' is not required but is so common + we've decided to unify it to some degree. + + Function 'list_instances_func' is called only if 'shared_key' is not + available in 'collection_shared_data' on creator. + + Args: + creator (Creator): Plugin which would like to get instance data. + shared_key (str): Key under which output of function will be stored. + list_instances_func (Function): Function that will return instance data + if data were not yet stored under 'shared_key'. + + Returns: + Dict[str, Dict[str, Any]]: Cached instances by creator identifier from + result of passed function. + """ + + if shared_key not in creator.collection_shared_data: + value = collections.defaultdict(list) + for instance in list_instances_func(): + identifier = instance.get("creator_identifier") + value[identifier].append(instance) + creator.collection_shared_data[shared_key] = value + return creator.collection_shared_data[shared_key] diff --git a/openpype/tools/experimental_tools/tools_def.py b/openpype/tools/experimental_tools/tools_def.py index d3a1caa60ed..3275e7b87db 100644 --- a/openpype/tools/experimental_tools/tools_def.py +++ b/openpype/tools/experimental_tools/tools_def.py @@ -88,7 +88,10 @@ def __init__(self, parent_widget=None, refresh=True): "publisher", "New publisher", "Combined creation and publishing into one tool.", - self._show_publisher + self._show_publisher, + hosts_filter=["blender", "maya", "nuke", "celaction", "flame", + "fusion", "harmony", "hiero", "resolve", + "tvpaint", "unreal"] ), ExperimentalTool( "traypublisher", diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index d37ce500e0f..826c7110da8 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -515,7 +515,7 @@ def on_context_menu(self, point): if not one_item_selected: # Filter loaders from first subset by intersected combinations for repre, loader in first_loaders: - if (repre["name"], loader) not in found_combinations: + if (repre["name"].lower(), loader) not in found_combinations: continue loaders.append((repre, loader)) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index a3387043b80..d8a69bbeb01 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -225,6 +225,12 @@ def __init__(self, parent=None, controller=None, reset_on_show=None): # Floating publish frame publish_frame = PublishFrame(controller, self.footer_border, self) + # Timer started on show -> connected to timer counter + # - helps to deffer on show logic by 3 event loops + show_timer = QtCore.QTimer() + show_timer.setInterval(1) + show_timer.timeout.connect(self._on_show_timer) + errors_dialog_message_timer = QtCore.QTimer() errors_dialog_message_timer.setInterval(100) errors_dialog_message_timer.timeout.connect( @@ -329,7 +335,6 @@ def __init__(self, parent=None, controller=None, reset_on_show=None): # forin init self._reset_on_first_show = reset_on_show self._reset_on_show = True - self._restart_timer = None self._publish_frame_visible = None self._error_messages_to_show = collections.deque() @@ -337,6 +342,9 @@ def __init__(self, parent=None, controller=None, reset_on_show=None): self._set_publish_visibility(False) + self._show_timer = show_timer + self._show_counter = 0 + @property def controller(self): return self._controller @@ -347,17 +355,7 @@ def showEvent(self, event): self._first_show = False self._on_first_show() - if not self._reset_on_show: - return - - self._reset_on_show = False - # Detach showing - give OS chance to draw the window - timer = QtCore.QTimer() - timer.setSingleShot(True) - timer.setInterval(1) - timer.timeout.connect(self._on_show_restart_timer) - self._restart_timer = timer - timer.start() + self._show_timer.start() def resizeEvent(self, event): super(PublisherWindow, self).resizeEvent(event) @@ -374,11 +372,21 @@ def _on_first_show(self): self.setStyleSheet(style.load_stylesheet()) self._reset_on_show = self._reset_on_first_show - def _on_show_restart_timer(self): - """Callback for '_restart_timer' timer.""" + def _on_show_timer(self): + # Add 1 to counter until hits 2 + if self._show_counter < 3: + self._show_counter += 1 + return - self._restart_timer = None - self.reset() + # Stop the timer + self._show_timer.stop() + # Reset counter when done for next show event + self._show_counter = 0 + + # Reset if requested + if self._reset_on_show: + self._reset_on_show = False + self.reset() def closeEvent(self, event): self.save_changes() diff --git a/openpype/version.py b/openpype/version.py index 442c5f033ba..46bb4b1cd0e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.14.7-nightly.1" +__version__ = "3.14.7-nightly.2" diff --git a/website/docs/artist_hosts_photoshop.md b/website/docs/artist_hosts_photoshop.md index 36670054eec..66efd64176b 100644 --- a/website/docs/artist_hosts_photoshop.md +++ b/website/docs/artist_hosts_photoshop.md @@ -22,72 +22,104 @@ When you launch Photoshop you will be met with the Workfiles app. If dont have a In Photoshop you can find the tools in the `OpenPype` extension: -![Extension](assets/photoshop_extension.PNG) +![Extension](assets/photoshop_extension.png) You can show the extension panel by going to `Window` > `Extensions` > `OpenPype`. -### Create +### Publish -When you have created an image you want to publish, you will need to create special groups or tag existing groups. To do this open the `Creator` through the extensions `Create` button. +When you are ready to share some work, you will need to publish. This is done by opening the `Publisher` through the `Publish...` button. -![Creator](assets/photoshop_creator.PNG) +![Publish](assets/photoshop_publish.png) -With the `Creator` you have a variety of options to create: +There is always instance for workfile created automatically (see 'workfileArt' item in `Subsets to publish` column.) This allows to publish (and therefore backup) +workfile which is used to produce another publishable elements (as `image` and `review` items). -- Check `Use selection` (A dialog will ask whether you want to create one image per selected layer). - - Yes. - - No selection. - - This will create a single group named after the `Subset` in the `Creator`. - - Single selected layer. - - The selected layer will be grouped under a single group named after the selected layer. - - Single selected group. - - The selected group will be tagged for publishing. - - Multiple selected items. - - Each selected group will be tagged for publishing and each layer will be grouped individually. - - No. - - All selected layers will be grouped under a single group named after the `Subset` in the `Creator`. -- Uncheck `Use selection`. - - This will create a single group named after the `Subset` in the `Creator`. +#### Create -#### Simplified publish +Main publishable item in Photoshop will be of `image` family. Result of this item (instance) is picture that could be loaded and used in another DCCs (for example as +single layer in composition in AfterEffects, reference in Maya etc). -There is a simplified workflow for simple use case where only single image should be created containing all visible layers. -No image instances must be present in a workfile and `project_settings/photoshop/publish/CollectInstances/flatten_subset_template` must be filled in Settings. -Then artists just need to hit 'Publish' button in menu. +There are couple of options what to publish: +- separate image per layer (or group of layers) +- all visible layers (groups) flattened into single image -### Publish +In most cases you would like to keep `Create only for selected` toggled on and select what you would like to publish. Toggling this off +will allow you to create instance(s) for all visible layers without a need to select them explicitly. + +For separate layers option keep `Create separate instance for each selected` toggled, select multiple layers and hit `Create >>>` button in the middle column. + +This will result in: + +![Image instances creates](assets/photoshop_publish_images.png) + +(In Photoshop's `Layers` tab standard layers will be wrapped into group and enriched with ℗ symbol to denote publishable instance. With `Create separate instance for each selected` toggled off +it will create only single publishable instance which will wrap all visible layers.) + +Name of publishable instance (eg. subset name) could be configured with a template in `project_settings/global/tools/creator/subset_name_profiles`. +(This must be configured by admin who has access to Openpype Settings.) + +Trash icon under the list of instances allows to delete any selected `image` instance. + +Workfile instance will be automatically recreated though. If you do not want to publish it, use pill toggle on the instance item. + +If you would like to modify publishable instance, click on `Publish` tab at the top. This would allow you to change name of publishable +instances, disable them from publishing, change their task etc. -When you are ready to share some work, you will need to publish. This is done by opening the `Pyblish` through the extensions `Publish` button. +Publisher allows publishing into different context, just click on any instance, update `Variant`, `Asset` or `Task` in the form in the middle and don't forget to click on the 'Confirm' button. -![Publish](assets/photoshop_publish.PNG) +#### Validate -This tool will run through checks to make sure the contents you are publishing is correct. Hit the "Play" button to start publishing. +If you would like to run validation rules set by your Studio, click on funnel icon at the bottom right. This will run through all +enabled instances, you could see more information after clicking on `Details` tab. + +![Image instances creates](assets/photoshop_publish_validations.png) + +In this dialog you could see publishable instances in left colummn, triggered plugins in the middle and logs in the right column. + +In left column you could see that `review` instance was created automatically. This instance flattens all publishable instances or +all visible layers if no publishable instances were created into single image which could serve as a single reviewable element (for example in Ftrack). + +Creation of Review could be disabled in `project_settings/photoshop/publish/CollectReview`. + +If you are satisfied with results of validation phase (and there are no errors there), you might hit `Publish` button at bottom right. +This will run through extraction phase (it physically creates images from `image` instances, creates `review` etc) and publishes them +(eg. stores files into their final destination and stores metadata about them into DB). +This part might take a while depending on amount of layers in the workfile, amount of available memory and performance of your machine. You may encounter issues with publishing which will be indicated with red squares. If these issues are within the validation section, then you can fix the issue. If there are issues outside of validation section, please let the OpenPype team know. -#### Repair Validation Issues +You can always start new publish run with a circle arrow button at the bottom right. You might also want to move between phases (Create, Update etc) +by clicking on available tabs at the top of the dialog. -All validators will give some description about what the issue is. You can inspect this by going into the validator through the arrow: +#### Simplified publish -![Inspect](assets/photoshop_publish_inspect.PNG) +There is a simplified workflow for simple use case where only single image should be created containing all visible layers. +No image instances must be present in a workfile and `project_settings/photoshop/publish/CollectInstances/flatten_subset_template` must be filled in Settings. +Then artists just need to hit 'Publish' button in menu. -You can expand the errors by clicking on them for more details: +#### Repair Validation Issues -![Expand](assets/photoshop_publish_expand.PNG) +If there is some issue in validator phase, you will receive something like this: -Some validator have repair actions, which will fix the issue. If you can identify validators with actions by the circle icon with an "A": +![Validation error](assets/photoshop_publish_failed.png) -![Actions](assets/photoshop_publish_actions.PNG) +All validators will give some description about what the issue is. You can inspect this by clicking on items in the left column. -To access the actions, you right click on the validator. If an action runs successfully, the actions icon will turn green. Once all issues are fixed, you can just hit the "Refresh" button and try to publish again. +If there is an option of automatic repair, there will be `Repair` button on the right. In other case you need to fix the issue manually. +(By deleting and recreating instance etc.) -![Repair](assets/photoshop_publish_repair.gif) +#### Buttons on the bottom right are for: +- `Refresh publishing` - set publishing process to starting position - useful if previous publish failed, or you changed configuration of a publish +- `Stop/pause publishing` - if you would like to pause publishing process at any time +- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet) +- `Publish` - standard way how to kick off full publishing process ### Load When you want to load existing published work, you can load in smart layers through the `Loader`. You can reach the `Loader` through the extension's `Load` button. -![Loader](assets/photoshop_loader.PNG) +![Loader](assets/photoshop_loader.png) The supported families for Photoshop are: @@ -105,7 +137,7 @@ Now that we have some images loaded, we can manage which version is loaded. This Loaded images has to stay as smart layers in order to be updated. If you rasterize the layer, you cannot update it to a different version. ::: -![Loader](assets/photoshop_manage.PNG) +![Loader](assets/photoshop_manage.png) You can switch to a previous version of the image or update to the latest. @@ -113,65 +145,19 @@ You can switch to a previous version of the image or update to the latest. ![Loader](assets/photoshop_manage_update.gif) -### New Publisher - -All previous screenshot came from regular [pyblish](https://pyblish.com/) process, there is also a different UI available. This process extends existing implementation and adds new functionalities. - -To test this in Photoshop, the artist needs first to enable experimental `New publisher` in Settings. (Tray > Settings > Experimental tools) -![Settings](assets/experimental_tools_settings.png) - -New dialog opens after clicking on `Experimental tools` button in Openpype extension menu. -![Menu](assets/experimental_tools_menu.png) - -After you click on this button, this dialog will show up. - -![Menu](assets/artist_photoshop_new_publisher_workfile.png) - -You can see the first instance, called `workfileYourTaskName`. (Name depends on studio naming convention for Photoshop's workfiles.). This instance is so called "automatic", -it was created without instigation by the artist. You shouldn't delete this instance as it might hold necessary values for future publishing, but you can choose to skip it -from publishing (by toggling the pill button inside of the rectangular object denoting instance). - -New publisher allows publishing into different context, just click on a workfile instance, update `Variant`, `Asset` or `Task` in the form in the middle and don't forget to click on the 'Confirm' button. - -Similarly to the old publishing approach, you need to create instances for everything you want to publish. You will initiate by clicking on the '+' sign in the bottom left corner. - -![Instance creator](assets/artist_photoshop_new_publisher_instance.png) - -In this dialog you can select the family for the published layer or group. Currently only 'image' is implemented. - -On right hand side you can see creator attributes: -- `Create only for selected` - mimics `Use selected` option of regular publish -- `Create separate instance for each selected` - if separate instance should be created for each layer if multiple selected - -![Instance created](assets/artist_photoshop_new_publisher_instance_created.png) - -Here you can see a newly created instance of image family. (Name depends on studio naming convention for image family.) You can disable instance from publishing in the same fashion as a workfile instance. -You could also decide delete instance by selecting it and clicking on a trashcan icon (next to plus button on left button) - -Buttons on the bottom right are for: -- `Refresh publishing` - set publishing process to starting position - useful if previous publish failed, or you changed configuration of a publish -- `Stop/pause publishing` - if you would like to pause publishing process at any time -- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet) -- `Publish` - standard way how to kick off full publishing process - -In the unfortunate case of some error during publishing, you would receive this kind of error dialog. - -![Publish failed](assets/artist_photoshop_new_publisher_publish_failed.png) - -In this case there is an issue that you are publishing two or more instances with the same subset name ('imageMaing'). If the error is recoverable by the artist, you should -see helpful information in a `How to repair?` section or fix it automatically by clicking on a 'Wrench' button on the right if present. - -If you would like to ask for help admin or support, you could use any of the three buttons on bottom left: +#### Support help +If you would like to ask for help admin or support, you could use any of the three options on the `Note` button on bottom left: +- `Go to details` - switches into a more detailed list of published instances and plugins. - `Copy report` - stash full publishing log to a clipboard -- `Export and save report` - save log into a file for sending it via mail or any communication tool -- `Show details` - switches into a more detailed list of published instances and plugins. Similar to the old pyblish list. +- `Export report` - save log into a file for sending it via mail or any communication tool If you are able to fix the workfile yourself, use the first button on the right to set the UI to initial state before publish. (Click the `Publish` button to start again.) +#### Legacy instances + +All screenshots from Publish are from updated dialog, before publishing was being done by regular `Pyblish` tool. New publishing process should be backward compatible, eg. if you have a workfile with instances created in the previous publishing approach, they will be translated automatically and could be used right away. -If you would create instances in a new publisher, you cannot use them in the old approach though! - -If you would hit on unexpected behaviour with old instances, contact support first, then you could try some steps to recover your publish. Delete instances in New publisher UI, or try `Subset manager` in the extension menu. +If you hit on unexpected behaviour with old instances, contact support first, then you could try to delete and recreate instances from scratch. Nuclear option is to purge workfile metadata in `File > File Info > Origin > Headline`. This is only for most determined daredevils though! diff --git a/website/docs/assets/artist_photoshop_new_publisher_instance.png b/website/docs/assets/artist_photoshop_new_publisher_instance.png deleted file mode 100644 index 723a032c944..00000000000 Binary files a/website/docs/assets/artist_photoshop_new_publisher_instance.png and /dev/null differ diff --git a/website/docs/assets/artist_photoshop_new_publisher_instance_created.png b/website/docs/assets/artist_photoshop_new_publisher_instance_created.png deleted file mode 100644 index 0cf6d1d18c3..00000000000 Binary files a/website/docs/assets/artist_photoshop_new_publisher_instance_created.png and /dev/null differ diff --git a/website/docs/assets/artist_photoshop_new_publisher_publish_failed.png b/website/docs/assets/artist_photoshop_new_publisher_publish_failed.png deleted file mode 100644 index e34497b77da..00000000000 Binary files a/website/docs/assets/artist_photoshop_new_publisher_publish_failed.png and /dev/null differ diff --git a/website/docs/assets/artist_photoshop_new_publisher_workfile.png b/website/docs/assets/artist_photoshop_new_publisher_workfile.png deleted file mode 100644 index 006206519f2..00000000000 Binary files a/website/docs/assets/artist_photoshop_new_publisher_workfile.png and /dev/null differ diff --git a/website/docs/assets/photoshop_creator.PNG b/website/docs/assets/photoshop_creator.png similarity index 100% rename from website/docs/assets/photoshop_creator.PNG rename to website/docs/assets/photoshop_creator.png diff --git a/website/docs/assets/photoshop_extension.PNG b/website/docs/assets/photoshop_extension.PNG deleted file mode 100644 index ef7081443df..00000000000 Binary files a/website/docs/assets/photoshop_extension.PNG and /dev/null differ diff --git a/website/docs/assets/photoshop_extension.png b/website/docs/assets/photoshop_extension.png new file mode 100644 index 00000000000..1f5c1792e1c Binary files /dev/null and b/website/docs/assets/photoshop_extension.png differ diff --git a/website/docs/assets/photoshop_loader.PNG b/website/docs/assets/photoshop_loader.png similarity index 100% rename from website/docs/assets/photoshop_loader.PNG rename to website/docs/assets/photoshop_loader.png diff --git a/website/docs/assets/photoshop_manage.PNG b/website/docs/assets/photoshop_manage.png similarity index 100% rename from website/docs/assets/photoshop_manage.PNG rename to website/docs/assets/photoshop_manage.png diff --git a/website/docs/assets/photoshop_publish.PNG b/website/docs/assets/photoshop_publish.PNG deleted file mode 100644 index dc577571227..00000000000 Binary files a/website/docs/assets/photoshop_publish.PNG and /dev/null differ diff --git a/website/docs/assets/photoshop_publish.png b/website/docs/assets/photoshop_publish.png new file mode 100644 index 00000000000..23b61cc6093 Binary files /dev/null and b/website/docs/assets/photoshop_publish.png differ diff --git a/website/docs/assets/photoshop_publish_actions.PNG b/website/docs/assets/photoshop_publish_actions.PNG deleted file mode 100644 index 86083ad54b9..00000000000 Binary files a/website/docs/assets/photoshop_publish_actions.PNG and /dev/null differ diff --git a/website/docs/assets/photoshop_publish_expand.PNG b/website/docs/assets/photoshop_publish_expand.PNG deleted file mode 100644 index 6969b156476..00000000000 Binary files a/website/docs/assets/photoshop_publish_expand.PNG and /dev/null differ diff --git a/website/docs/assets/photoshop_publish_failed.png b/website/docs/assets/photoshop_publish_failed.png new file mode 100644 index 00000000000..5aaafcbda29 Binary files /dev/null and b/website/docs/assets/photoshop_publish_failed.png differ diff --git a/website/docs/assets/photoshop_publish_images.png b/website/docs/assets/photoshop_publish_images.png new file mode 100644 index 00000000000..34f0b4755c6 Binary files /dev/null and b/website/docs/assets/photoshop_publish_images.png differ diff --git a/website/docs/assets/photoshop_publish_inspect.PNG b/website/docs/assets/photoshop_publish_inspect.PNG deleted file mode 100644 index d2fd8922af1..00000000000 Binary files a/website/docs/assets/photoshop_publish_inspect.PNG and /dev/null differ diff --git a/website/docs/assets/photoshop_publish_repair.gif b/website/docs/assets/photoshop_publish_repair.gif deleted file mode 100644 index bf7065801ec..00000000000 Binary files a/website/docs/assets/photoshop_publish_repair.gif and /dev/null differ diff --git a/website/docs/assets/photoshop_publish_validations.png b/website/docs/assets/photoshop_publish_validations.png new file mode 100644 index 00000000000..2260c8d50b1 Binary files /dev/null and b/website/docs/assets/photoshop_publish_validations.png differ