From ffa3d34f7a5c31e9fdf4ae268224dae3e6ea206c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Oct 2020 23:24:09 +0200 Subject: [PATCH 1/8] moved "-shortest" argument to better spot --- pype/plugins/global/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index f4a39a7c31..dafd2e3855 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -341,8 +341,6 @@ class ExtractReview(pyblish.api.InstancePlugin): duration_sec = float(output_frames_len / temp_data["fps"]) ffmpeg_output_args.append("-t {:0.2f}".format(duration_sec)) - # Use shortest input - ffmpeg_output_args.append("-shortest") # Add video/image input path ffmpeg_input_args.append( @@ -354,6 +352,8 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_input_args.append( "-start_number {}".format(temp_data["output_frame_start"]) ) + # Use shortest input + ffmpeg_output_args.append("-shortest") # Add audio arguments if there are any. Skipped when output are images. if not temp_data["output_ext_is_image"]: From b964218fc2ee17027d7d84176a066273e5ee9d9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Oct 2020 23:24:37 +0200 Subject: [PATCH 2/8] safer handle values getting --- pype/plugins/global/publish/extract_review.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index dafd2e3855..21680177c3 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -238,15 +238,16 @@ class ExtractReview(pyblish.api.InstancePlugin): """ frame_start = instance.data["frameStart"] - handle_start = instance.data.get( - "handleStart", - instance.context.data["handleStart"] - ) frame_end = instance.data["frameEnd"] - handle_end = instance.data.get( - "handleEnd", - instance.context.data["handleEnd"] - ) + + # Try to get handles from instance + handle_start = instance.data.get("handleStart") + handle_end = instance.data.get("handleEnd") + # If even one of handle values is not set on instance use + # handles from context + if handle_start is None or handle_end is None: + handle_start = instance.context.data["handleStart"] + handle_end = instance.context.data["handleEnd"] frame_start_handle = frame_start - handle_start frame_end_handle = frame_end + handle_end From 3af13c031e7db2d87576eec3742adeda425c6f9b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Oct 2020 23:24:49 +0200 Subject: [PATCH 3/8] store if handles are even set --- pype/plugins/global/publish/extract_review.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 21680177c3..4295842f48 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -261,6 +261,8 @@ class ExtractReview(pyblish.api.InstancePlugin): output_frame_start = frame_start_handle output_frame_end = frame_end_handle + handles_are_set = handle_start > 0 or handle_end > 0 + return { "fps": float(instance.data["fps"]), "frame_start": frame_start, @@ -276,7 +278,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "resolution_height": instance.data.get("resolutionHeight"), "origin_repre": repre, "input_is_sequence": self.input_is_sequence(repre), - "without_handles": without_handles + "without_handles": without_handles, + "handles_are_set": handles_are_set } def _ffmpeg_arguments(self, output_def, instance, new_repre, temp_data): From c44d0ab339074e61f5b6da48fa0cb8f39bbcda4d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Oct 2020 23:26:07 +0200 Subject: [PATCH 4/8] setting offset and duration by seconds is based only on handles --- pype/plugins/global/publish/extract_review.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 4295842f48..c76d205284 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -338,10 +338,16 @@ class ExtractReview(pyblish.api.InstancePlugin): "-framerate {}".format(temp_data["fps"]) ) - elif temp_data["without_handles"]: - start_sec = float(temp_data["handle_start"]) / temp_data["fps"] - ffmpeg_input_args.append("-ss {:0.2f}".format(start_sec)) + # Change output's duration and start point if should not contain + # handles + if temp_data["without_handles"] and temp_data["handles_are_set"]: + # Set start time without handles + # - check if handle_start is bigger than 0 to avoid zero division + if temp_data["handle_start"] > 0: + start_sec = float(temp_data["handle_start"]) / temp_data["fps"] + ffmpeg_input_args.append("-ss {:0.2f}".format(start_sec)) + # Set output duration inn seconds duration_sec = float(output_frames_len / temp_data["fps"]) ffmpeg_output_args.append("-t {:0.2f}".format(duration_sec)) From c826aaa0a64d1a33dc1f9136fb307513365d8455 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Oct 2020 23:26:26 +0200 Subject: [PATCH 5/8] add video length if input or output is sequence --- pype/plugins/global/publish/extract_review.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index c76d205284..68cbe431ff 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -351,6 +351,9 @@ class ExtractReview(pyblish.api.InstancePlugin): duration_sec = float(output_frames_len / temp_data["fps"]) ffmpeg_output_args.append("-t {:0.2f}".format(duration_sec)) + # Set frame range of output when input or output is sequence + elif temp_data["input_is_sequence"] or temp_data["output_is_sequence"]: + ffmpeg_output_args.append("-frames:v {}".format(output_frames_len)) # Add video/image input path ffmpeg_input_args.append( From 58648a87cd0b8dd6efbf5e80fa2123ecaf1a3ad0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Oct 2020 23:26:42 +0200 Subject: [PATCH 6/8] moved setting output start frame to better spot --- pype/plugins/global/publish/extract_review.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 68cbe431ff..b88bb82c42 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -338,6 +338,13 @@ class ExtractReview(pyblish.api.InstancePlugin): "-framerate {}".format(temp_data["fps"]) ) + if temp_data["output_is_sequence"]: + # Set start frame of output sequence (just frame in filename) + # - this is definition of an output + ffmpeg_output_args.append( + "-start_number {}".format(temp_data["output_frame_start"]) + ) + # Change output's duration and start point if should not contain # handles if temp_data["without_handles"] and temp_data["handles_are_set"]: @@ -360,11 +367,6 @@ class ExtractReview(pyblish.api.InstancePlugin): "-i \"{}\"".format(temp_data["full_input_path"]) ) - if temp_data["output_is_sequence"]: - # Set start frame - ffmpeg_input_args.append( - "-start_number {}".format(temp_data["output_frame_start"]) - ) # Use shortest input ffmpeg_output_args.append("-shortest") From aeeaba9bc8ab71a4db79959e71e55b4139cbe621 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 13 Oct 2020 23:26:48 +0200 Subject: [PATCH 7/8] added comment --- pype/plugins/global/publish/extract_review.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index b88bb82c42..4c31ddf0f9 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -322,7 +322,8 @@ class ExtractReview(pyblish.api.InstancePlugin): ) if temp_data["input_is_sequence"]: - # Set start frame + # Set start frame of input sequence (just frame in filename) + # - definition of input filepath ffmpeg_input_args.append( "-start_number {}".format(temp_data["output_frame_start"]) ) From e557e4cfc9d1081c880d435b944a4fb15484d50f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 20 Oct 2020 18:54:26 +0200 Subject: [PATCH 8/8] #654 - Fix Layer name is not propagating Fixed saving namespace into Headline Fixed switch asset and update version Added delete layer by id --- .../stubs/photoshop_server_stub.py | 102 +++++++++++------- pype/plugins/photoshop/load/load_image.py | 54 +++++++++- 2 files changed, 114 insertions(+), 42 deletions(-) diff --git a/pype/modules/websocket_server/stubs/photoshop_server_stub.py b/pype/modules/websocket_server/stubs/photoshop_server_stub.py index da69127799..04fb7eff0f 100644 --- a/pype/modules/websocket_server/stubs/photoshop_server_stub.py +++ b/pype/modules/websocket_server/stubs/photoshop_server_stub.py @@ -22,8 +22,9 @@ class PhotoshopServerStub(): def open(self, path): """ Open file located at 'path' (local). - :param path: file path locally - :return: None + Args: + path(string): file path locally + Returns: None """ self.websocketserver.call(self.client.call ('Photoshop.open', path=path) @@ -32,9 +33,10 @@ class PhotoshopServerStub(): def read(self, layer, layers_meta=None): """ Parses layer metadata from Headline field of active document - :param layer: Layer("id": XXX, "name":'YYY') - :param data: json representation for single layer - :param all_layers: - for performance, could be + Args: + layer (namedtuple): Layer("id": XXX, "name":'YYY') + data(string): json representation for single layer + all_layers (list of namedtuples): for performance, could be injected for usage in loop, if not, single call will be triggered - :param layers_meta: json representation from Headline + layers_meta(string): json representation from Headline (for performance - provide only if imprint is in loop - value should be same) - :return: None + Returns: None """ if not layers_meta: layers_meta = self.get_layers_metadata() # json.dumps writes integer values in a dictionary to string, so # anticipating it here. if str(layer.id) in layers_meta and layers_meta[str(layer.id)]: - layers_meta[str(layer.id)].update(data) + if data: + layers_meta[str(layer.id)].update(data) + else: + layers_meta.pop(str(layer.id)) else: layers_meta[str(layer.id)] = data @@ -83,7 +89,7 @@ class PhotoshopServerStub(): """ Returns JSON document with all(?) layers in active document. - :return: + Returns: Format of tuple: { 'id':'123', 'name': 'My Layer 1', 'type': 'GUIDE'|'FG'|'BG'|'OBJ' @@ -97,8 +103,9 @@ class PhotoshopServerStub(): def get_layers_in_layers(self, layers): """ Return all layers that belong to layers (might be groups). - :param layers: - :return: + Args: + layers : + Returns: """ all_layers = self.get_layers() ret = [] @@ -116,7 +123,7 @@ class PhotoshopServerStub(): def create_group(self, name): """ Create new group (eg. LayerSet) - :return: + Returns: """ ret = self.websocketserver.call(self.client.call ('Photoshop.create_group', @@ -128,7 +135,7 @@ class PhotoshopServerStub(): def group_selected_layers(self, name): """ Group selected layers into new LayerSet (eg. group) - :return: + Returns: """ res = self.websocketserver.call(self.client.call ('Photoshop.group_selected_layers', @@ -139,7 +146,7 @@ class PhotoshopServerStub(): def get_selected_layers(self): """ Get a list of actually selected layers - :return: + Returns: """ res = self.websocketserver.call(self.client.call ('Photoshop.get_selected_layers')) @@ -147,9 +154,10 @@ class PhotoshopServerStub(): def select_layers(self, layers): """ - Selecte specified layers in Photoshop - :param layers: - :return: None + Selects specified layers in Photoshop by its ids + Args: + layers: + Returns: None """ layer_ids = [layer.id for layer in layers] @@ -161,7 +169,7 @@ class PhotoshopServerStub(): def get_active_document_full_name(self): """ Returns full name with path of active document via ws call - :return: full path with name + Returns(string): full path with name """ res = self.websocketserver.call( self.client.call('Photoshop.get_active_document_full_name')) @@ -171,7 +179,7 @@ class PhotoshopServerStub(): def get_active_document_name(self): """ Returns just a name of active document via ws call - :return: file name + Returns(string): file name """ res = self.websocketserver.call(self.client.call ('Photoshop.get_active_document_name')) @@ -181,7 +189,7 @@ class PhotoshopServerStub(): def is_saved(self): """ Returns true if no changes in active document - :return: + Returns: """ return self.websocketserver.call(self.client.call ('Photoshop.is_saved')) @@ -189,7 +197,7 @@ class PhotoshopServerStub(): def save(self): """ Saves active document - :return: None + Returns: None """ self.websocketserver.call(self.client.call ('Photoshop.save')) @@ -197,10 +205,11 @@ class PhotoshopServerStub(): def saveAs(self, image_path, ext, as_copy): """ Saves active document to psd (copy) or png or jpg - :param image_path: full local path - :param ext: - :param as_copy: - :return: None + Args: + image_path(string): full local path + ext: + as_copy: + Returns: None """ self.websocketserver.call(self.client.call ('Photoshop.saveAs', @@ -211,9 +220,10 @@ class PhotoshopServerStub(): def set_visible(self, layer_id, visibility): """ Set layer with 'layer_id' to 'visibility' - :param layer_id: - :param visibility: - :return: None + Args: + layer_id: + visibility: + Returns: None """ self.websocketserver.call(self.client.call ('Photoshop.set_visible', @@ -224,7 +234,7 @@ class PhotoshopServerStub(): """ Reads layers metadata from Headline from active document in PS. (Headline accessible by File > File Info) - :return: - json documents + Returns(string): - json documents """ layers_data = {} res = self.websocketserver.call(self.client.call('Photoshop.read')) @@ -234,22 +244,26 @@ class PhotoshopServerStub(): pass return layers_data - def import_smart_object(self, path): + def import_smart_object(self, path, layer_name): """ Import the file at `path` as a smart object to active document. Args: path (str): File path to import. + layer_name (str): Unique layer name to differentiate how many times + same smart object was loaded """ res = self.websocketserver.call(self.client.call ('Photoshop.import_smart_object', - path=path)) + path=path, name=layer_name)) return self._to_records(res).pop() - def replace_smart_object(self, layer, path): + def replace_smart_object(self, layer, path, layer_name): """ Replace the smart object `layer` with file at `path` + layer_name (str): Unique layer name to differentiate how many times + same smart object was loaded Args: layer (namedTuple): Layer("id":XX, "name":"YY"..). @@ -257,8 +271,18 @@ class PhotoshopServerStub(): """ self.websocketserver.call(self.client.call ('Photoshop.replace_smart_object', - layer=layer, - path=path)) + layer_id=layer.id, + path=path, name=layer_name)) + + def delete_layer(self, layer_id): + """ + Deletes specific layer by it's id. + Args: + layer_id (int): id of layer to delete + """ + self.websocketserver.call(self.client.call + ('Photoshop.delete_layer', + layer_id=layer_id)) def close(self): self.client.close() @@ -267,8 +291,8 @@ class PhotoshopServerStub(): """ Converts string json representation into list of named tuples for dot notation access to work. - :return: - :param res: - json representation + Returns: + res(string): - json representation """ try: layers_data = json.loads(res) diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py index 75c02bb327..301e60fbb1 100644 --- a/pype/plugins/photoshop/load/load_image.py +++ b/pype/plugins/photoshop/load/load_image.py @@ -1,4 +1,6 @@ from avalon import api, photoshop +import os +import re stub = photoshop.stub() @@ -13,10 +15,13 @@ class ImageLoader(api.Loader): representations = ["*"] def load(self, context, name=None, namespace=None, data=None): + layer_name = self._get_unique_layer_name(context["asset"]["name"], + name) with photoshop.maintained_selection(): - layer = stub.import_smart_object(self.fname) + layer = stub.import_smart_object(self.fname, layer_name) self[:] = [layer] + namespace = namespace or layer_name return photoshop.containerise( name, @@ -27,11 +32,25 @@ class ImageLoader(api.Loader): ) def update(self, container, representation): + """ Switch asset or change version """ layer = container.pop("layer") + context = representation.get("context", {}) + + namespace_from_container = re.sub(r'_\d{3}$', '', + container["namespace"]) + layer_name = "{}_{}".format(context["asset"], context["subset"]) + # switching assets + if namespace_from_container != layer_name: + layer_name = self._get_unique_layer_name(context["asset"], + context["subset"]) + else: # switching version - keep same name + layer_name = container["namespace"] + + path = api.get_representation_path(representation) with photoshop.maintained_selection(): stub.replace_smart_object( - layer, api.get_representation_path(representation) + layer, path, layer_name ) stub.imprint( @@ -39,7 +58,36 @@ class ImageLoader(api.Loader): ) def remove(self, container): - container["layer"].Delete() + """ + Removes element from scene: deletes layer + removes from Headline + Args: + container (dict): container to be removed - used to get layer_id + """ + layer = container.pop("layer") + stub.imprint(layer, {}) + stub.delete_layer(layer.id) def switch(self, container, representation): self.update(container, representation) + + def _get_unique_layer_name(self, asset_name, subset_name): + """ + Gets all layer names and if 'name' is present in them, increases + suffix by 1 (eg. creates unique layer name - for Loader) + Args: + name (string): in format asset_subset + + Returns: + (string): name_00X (without version) + """ + name = "{}_{}".format(asset_name, subset_name) + names = {} + for layer in stub.get_layers(): + layer_name = re.sub(r'_\d{3}$', '', layer.name) + if layer_name in names.keys(): + names[layer_name] = names[layer_name] + 1 + else: + names[layer_name] = 1 + occurrences = names.get(name, 0) + + return "{}_{:0>3d}".format(name, occurrences + 1)