Photoshop: add autocreators for review and flat image (#4871)

* OP-5656 - added auto creator for review in PS

Review instance should be togglable.
Review instance needs to be created for non publisher based workflows.

* OP-5656 - refactored names

* OP-5656 - refactored names

* OP-5656 - new auto creator for flat image

In old version flat image was created if no instances were created. Explicit auto creator added for clarity.

Standardization of state of plugins

* OP-5656 - updated according to auto image creator

Subset template should be used from autocreator and not be separate.

* OP-5656 - fix proper creator name

* OP-5656 - fix log message

* OP-5656 - fix use enable state

* OP-5656 - fix formatting

* OP-5656 - add review toggle to image instance

For special cases where each image should have separate review.

* OP-5656 - fix description

* OP-5656 - fix not present asset and task in instance context

* OP-5656 - refactor - both auto creators should use same class

Provided separate description.

* OP-5656 - fix - propagate review to families

Image and auto image could have now review flag.
Bottom logic is only for Webpublisher.

* OP-5656 - fix - rename review files to avaid collision

Image family produces jpg and png, jpg review would clash with name. It should be replaced by 'jpg_jpg'.

* OP-5656 - fix - limit additional auto created only on WP

In artist based publishing auto image would be created by auto creator (if enabled). Artist might want to disable image creation.

* OP-5656 - added mark_for_review flag to Publish tab

* OP-5656 - fixes for auto creator

* OP-5656 - fixe - outputDef not needed

outputDef should contain dict of output definition. In PS it doesn't make sense as it has separate extract_review without output definitions.

* OP-5656 - added persistency of changes to auto creators

Changes as enabling/disabling, changing review flag should persist.

* OP-5656 - added documentation for admins

* OP-5656 - added link to new documentation for admins

* OP-5656 - Hound

* OP-5656 - Hound

* OP-5656 - fix shared families list

* OP-5656 - added default variant for review and workfile creator

For workfile Main was default variant, "" was for review.

* OP-5656 - fix - use values from Settings

* OP-5656 - fix - use original name of review for main review family

outputName cannot be in repre or file would have ..._jpg.jpg

* OP-5656 - refactor - standardized settings

Active by default denotes if created instance is active (eg. publishable) when created.

* OP-5656 - fixes for skipping collecting auto_image

data["ids"] are necessary for extracting. Members are physical layers in image, ids are "virtual" items, won't get grouped into real image instance.

* OP-5656 - reworked auto collectors

This allows to use automatic test for proper testing.

* OP-5656 - added automatic tests

* OP-5656 - fixes for auto collectors

* OP-5656 - removed unnecessary collector

Logic moved to auto collectors.

* OP-5656 - Hound
This commit is contained in:
Petr Kalis 2023-05-02 11:19:50 +02:00 committed by GitHub
parent 7dbf2cd613
commit b8ce6e9e9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1044 additions and 245 deletions

View file

@ -7,28 +7,26 @@ from openpype.pipeline import (
from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances
class PSWorkfileCreator(AutoCreator):
identifier = "workfile"
family = "workfile"
default_variant = "Main"
class PSAutoCreator(AutoCreator):
"""Generic autocreator to extend."""
def get_instance_attr_defs(self):
return []
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:
subset_name = instance_data["subset"]
instance = CreatedInstance(
self.family, subset_name, instance_data, self
instance = CreatedInstance.from_existing(
instance_data, self
)
self._add_instance_to_context(instance)
def update_instances(self, update_list):
# nothing to change on workfiles
pass
self.log.debug("update_list:: {}".format(update_list))
for created_inst, _changes in update_list:
api.stub().imprint(created_inst.get("instance_id"),
created_inst.data_to_store())
def create(self, options=None):
existing_instance = None
@ -58,6 +56,9 @@ class PSWorkfileCreator(AutoCreator):
project_name, host_name, None
))
if not self.active_on_create:
data["active"] = False
new_instance = CreatedInstance(
self.family, subset_name, data, self
)

View file

@ -0,0 +1,120 @@
from openpype.pipeline import CreatedInstance
from openpype.lib import BoolDef
import openpype.hosts.photoshop.api as api
from openpype.hosts.photoshop.lib import PSAutoCreator
from openpype.pipeline.create import get_subset_name
from openpype.client import get_asset_by_name
class AutoImageCreator(PSAutoCreator):
"""Creates flatten image from all visible layers.
Used in simplified publishing as auto created instance.
Must be enabled in Setting and template for subset name provided
"""
identifier = "auto_image"
family = "image"
# Settings
default_variant = ""
# - Mark by default instance for review
mark_for_review = True
active_on_create = True
def create(self, options=None):
existing_instance = None
for instance in self.create_context.instances:
if instance.creator_identifier == self.identifier:
existing_instance = instance
break
context = self.create_context
project_name = context.get_current_project_name()
asset_name = context.get_current_asset_name()
task_name = context.get_current_task_name()
host_name = context.host_name
asset_doc = get_asset_by_name(project_name, asset_name)
if existing_instance is None:
subset_name = get_subset_name(
self.family, self.default_variant, task_name, asset_doc,
project_name, host_name
)
publishable_ids = [layer.id for layer in api.stub().get_layers()
if layer.visible]
data = {
"asset": asset_name,
"task": task_name,
# ids are "virtual" layers, won't get grouped as 'members' do
# same difference in color coded layers in WP
"ids": publishable_ids
}
if not self.active_on_create:
data["active"] = False
creator_attributes = {"mark_for_review": self.mark_for_review}
data.update({"creator_attributes": creator_attributes})
new_instance = CreatedInstance(
self.family, subset_name, data, self
)
self._add_instance_to_context(new_instance)
api.stub().imprint(new_instance.get("instance_id"),
new_instance.data_to_store())
elif ( # existing instance from different context
existing_instance["asset"] != asset_name
or existing_instance["task"] != task_name
):
subset_name = get_subset_name(
self.family, self.default_variant, task_name, asset_doc,
project_name, host_name
)
existing_instance["asset"] = asset_name
existing_instance["task"] = task_name
existing_instance["subset"] = subset_name
api.stub().imprint(existing_instance.get("instance_id"),
existing_instance.data_to_store())
def get_pre_create_attr_defs(self):
return [
BoolDef(
"mark_for_review",
label="Review",
default=self.mark_for_review
)
]
def get_instance_attr_defs(self):
return [
BoolDef(
"mark_for_review",
label="Review"
)
]
def apply_settings(self, project_settings, system_settings):
plugin_settings = (
project_settings["photoshop"]["create"]["AutoImageCreator"]
)
self.active_on_create = plugin_settings["active_on_create"]
self.default_variant = plugin_settings["default_variant"]
self.mark_for_review = plugin_settings["mark_for_review"]
self.enabled = plugin_settings["enabled"]
def get_detail_description(self):
return """Creator for flatten image.
Studio might configure simple publishing workflow. In that case
`image` instance is automatically created which will publish flat
image from all visible layers.
Artist might disable this instance from publishing or from creating
review for it though.
"""

View file

@ -23,6 +23,11 @@ class ImageCreator(Creator):
family = "image"
description = "Image creator"
# Settings
default_variants = ""
mark_for_review = False
active_on_create = True
def create(self, subset_name_from_ui, data, pre_create_data):
groups_to_create = []
top_layers_to_wrap = []
@ -94,6 +99,12 @@ class ImageCreator(Creator):
data.update({"layer_name": layer_name})
data.update({"long_name": "_".join(layer_names_in_hierarchy)})
creator_attributes = {"mark_for_review": self.mark_for_review}
data.update({"creator_attributes": creator_attributes})
if not self.active_on_create:
data["active"] = False
new_instance = CreatedInstance(self.family, subset_name, data,
self)
@ -134,11 +145,6 @@ class ImageCreator(Creator):
self.host.remove_instance(instance)
self._remove_instance_from_context(instance)
def get_default_variants(self):
return [
"Main"
]
def get_pre_create_attr_defs(self):
output = [
BoolDef("use_selection", default=True,
@ -148,10 +154,34 @@ class ImageCreator(Creator):
label="Create separate instance for each selected"),
BoolDef("use_layer_name",
default=False,
label="Use layer name in subset")
label="Use layer name in subset"),
BoolDef(
"mark_for_review",
label="Create separate review",
default=False
)
]
return output
def get_instance_attr_defs(self):
return [
BoolDef(
"mark_for_review",
label="Review"
)
]
def apply_settings(self, project_settings, system_settings):
plugin_settings = (
project_settings["photoshop"]["create"]["ImageCreator"]
)
self.active_on_create = plugin_settings["active_on_create"]
self.default_variants = plugin_settings["default_variants"]
self.mark_for_review = plugin_settings["mark_for_review"]
self.enabled = plugin_settings["enabled"]
def get_detail_description(self):
return """Creator for Image instances
@ -180,6 +210,11 @@ class ImageCreator(Creator):
but layer name should be used (set explicitly in UI or implicitly if
multiple images should be created), it is added in capitalized form
as a suffix to subset name.
Each image could have its separate review created if necessary via
`Create separate review` toggle.
But more use case is to use separate `review` instance to create review
from all published items.
"""
def _handle_legacy(self, instance_data):

View file

@ -0,0 +1,28 @@
from openpype.hosts.photoshop.lib import PSAutoCreator
class ReviewCreator(PSAutoCreator):
"""Creates review instance which might be disabled from publishing."""
identifier = "review"
family = "review"
default_variant = "Main"
def get_detail_description(self):
return """Auto creator for review.
Photoshop review is created from all published images or from all
visible layers if no `image` instances got created.
Review might be disabled by an artist (instance shouldn't be deleted as
it will get recreated in next publish either way).
"""
def apply_settings(self, project_settings, system_settings):
plugin_settings = (
project_settings["photoshop"]["create"]["ReviewCreator"]
)
self.default_variant = plugin_settings["default_variant"]
self.active_on_create = plugin_settings["active_on_create"]
self.enabled = plugin_settings["enabled"]

View file

@ -0,0 +1,28 @@
from openpype.hosts.photoshop.lib import PSAutoCreator
class WorkfileCreator(PSAutoCreator):
identifier = "workfile"
family = "workfile"
default_variant = "Main"
def get_detail_description(self):
return """Auto creator for workfile.
It is expected that each publish will also publish its source workfile
for safekeeping. This creator triggers automatically without need for
an artist to remember and trigger it explicitly.
Workfile instance could be disabled if it is not required to publish
workfile. (Instance shouldn't be deleted though as it will be recreated
in next publish automatically).
"""
def apply_settings(self, project_settings, system_settings):
plugin_settings = (
project_settings["photoshop"]["create"]["WorkfileCreator"]
)
self.active_on_create = plugin_settings["active_on_create"]
self.enabled = plugin_settings["enabled"]

View file

@ -0,0 +1,101 @@
import pyblish.api
from openpype.hosts.photoshop import api as photoshop
from openpype.pipeline.create import get_subset_name
class CollectAutoImage(pyblish.api.ContextPlugin):
"""Creates auto image in non artist based publishes (Webpublisher).
'remotepublish' should be renamed to 'autopublish' or similar in the future
"""
label = "Collect Auto Image"
order = pyblish.api.CollectorOrder
hosts = ["photoshop"]
order = pyblish.api.CollectorOrder + 0.2
targets = ["remotepublish"]
def process(self, context):
family = "image"
for instance in context:
creator_identifier = instance.data.get("creator_identifier")
if creator_identifier and creator_identifier == "auto_image":
self.log.debug("Auto image instance found, won't create new")
return
project_name = context.data["anatomyData"]["project"]["name"]
proj_settings = context.data["project_settings"]
task_name = context.data["anatomyData"]["task"]["name"]
host_name = context.data["hostName"]
asset_doc = context.data["assetEntity"]
asset_name = asset_doc["name"]
auto_creator = proj_settings.get(
"photoshop", {}).get(
"create", {}).get(
"AutoImageCreator", {})
if not auto_creator or not auto_creator["enabled"]:
self.log.debug("Auto image creator disabled, won't create new")
return
stub = photoshop.stub()
stored_items = stub.get_layers_metadata()
for item in stored_items:
if item.get("creator_identifier") == "auto_image":
if not item.get("active"):
self.log.debug("Auto_image instance disabled")
return
layer_items = stub.get_layers()
publishable_ids = [layer.id for layer in layer_items
if layer.visible]
# collect stored image instances
instance_names = []
for layer_item in layer_items:
layer_meta_data = stub.read(layer_item, stored_items)
# Skip layers without metadata.
if layer_meta_data is None:
continue
# Skip containers.
if "container" in layer_meta_data["id"]:
continue
# active might not be in legacy meta
if layer_meta_data.get("active", True) and layer_item.visible:
instance_names.append(layer_meta_data["subset"])
if len(instance_names) == 0:
variants = proj_settings.get(
"photoshop", {}).get(
"create", {}).get(
"CreateImage", {}).get(
"default_variants", [''])
family = "image"
variant = context.data.get("variant") or variants[0]
subset_name = get_subset_name(
family, variant, task_name, asset_doc,
project_name, host_name
)
instance = context.create_instance(subset_name)
instance.data["family"] = family
instance.data["asset"] = asset_name
instance.data["subset"] = subset_name
instance.data["ids"] = publishable_ids
instance.data["publish"] = True
instance.data["creator_identifier"] = "auto_image"
if auto_creator["mark_for_review"]:
instance.data["creator_attributes"] = {"mark_for_review": True}
instance.data["families"] = ["review"]
self.log.info("auto image instance: {} ".format(instance.data))

View file

@ -0,0 +1,92 @@
"""
Requires:
None
Provides:
instance -> family ("review")
"""
import pyblish.api
from openpype.hosts.photoshop import api as photoshop
from openpype.pipeline.create import get_subset_name
class CollectAutoReview(pyblish.api.ContextPlugin):
"""Create review instance in non artist based workflow.
Called only if PS is triggered in Webpublisher or in tests.
"""
label = "Collect Auto Review"
hosts = ["photoshop"]
order = pyblish.api.CollectorOrder + 0.2
targets = ["remotepublish"]
publish = True
def process(self, context):
family = "review"
has_review = False
for instance in context:
if instance.data["family"] == family:
self.log.debug("Review instance found, won't create new")
has_review = True
creator_attributes = instance.data.get("creator_attributes", {})
if (creator_attributes.get("mark_for_review") and
"review" not in instance.data["families"]):
instance.data["families"].append("review")
if has_review:
return
stub = photoshop.stub()
stored_items = stub.get_layers_metadata()
for item in stored_items:
if item.get("creator_identifier") == family:
if not item.get("active"):
self.log.debug("Review instance disabled")
return
auto_creator = context.data["project_settings"].get(
"photoshop", {}).get(
"create", {}).get(
"ReviewCreator", {})
if not auto_creator or not auto_creator["enabled"]:
self.log.debug("Review creator disabled, won't create new")
return
variant = (context.data.get("variant") or
auto_creator["default_variant"])
project_name = context.data["anatomyData"]["project"]["name"]
proj_settings = context.data["project_settings"]
task_name = context.data["anatomyData"]["task"]["name"]
host_name = context.data["hostName"]
asset_doc = context.data["assetEntity"]
asset_name = asset_doc["name"]
subset_name = get_subset_name(
family,
variant,
task_name,
asset_doc,
project_name,
host_name=host_name,
project_settings=proj_settings
)
instance = context.create_instance(subset_name)
instance.data.update({
"subset": subset_name,
"label": subset_name,
"name": subset_name,
"family": family,
"families": [],
"representations": [],
"asset": asset_name,
"publish": self.publish
})
self.log.debug("auto review created::{}".format(instance.data))

View file

@ -0,0 +1,99 @@
import os
import pyblish.api
from openpype.hosts.photoshop import api as photoshop
from openpype.pipeline.create import get_subset_name
class CollectAutoWorkfile(pyblish.api.ContextPlugin):
"""Collect current script for publish."""
order = pyblish.api.CollectorOrder + 0.2
label = "Collect Workfile"
hosts = ["photoshop"]
targets = ["remotepublish"]
def process(self, context):
family = "workfile"
file_path = context.data["currentFile"]
_, ext = os.path.splitext(file_path)
staging_dir = os.path.dirname(file_path)
base_name = os.path.basename(file_path)
workfile_representation = {
"name": ext[1:],
"ext": ext[1:],
"files": base_name,
"stagingDir": staging_dir,
}
for instance in context:
if instance.data["family"] == family:
self.log.debug("Workfile instance found, won't create new")
instance.data.update({
"label": base_name,
"name": base_name,
"representations": [],
})
# creating representation
_, ext = os.path.splitext(file_path)
instance.data["representations"].append(
workfile_representation)
return
stub = photoshop.stub()
stored_items = stub.get_layers_metadata()
for item in stored_items:
if item.get("creator_identifier") == family:
if not item.get("active"):
self.log.debug("Workfile instance disabled")
return
project_name = context.data["anatomyData"]["project"]["name"]
proj_settings = context.data["project_settings"]
auto_creator = proj_settings.get(
"photoshop", {}).get(
"create", {}).get(
"WorkfileCreator", {})
if not auto_creator or not auto_creator["enabled"]:
self.log.debug("Workfile creator disabled, won't create new")
return
# context.data["variant"] might come only from collect_batch_data
variant = (context.data.get("variant") or
auto_creator["default_variant"])
task_name = context.data["anatomyData"]["task"]["name"]
host_name = context.data["hostName"]
asset_doc = context.data["assetEntity"]
asset_name = asset_doc["name"]
subset_name = get_subset_name(
family,
variant,
task_name,
asset_doc,
project_name,
host_name=host_name,
project_settings=proj_settings
)
# Create instance
instance = context.create_instance(subset_name)
instance.data.update({
"subset": subset_name,
"label": base_name,
"name": base_name,
"family": family,
"families": [],
"representations": [],
"asset": asset_name
})
# creating representation
instance.data["representations"].append(workfile_representation)
self.log.debug("auto workfile review created:{}".format(instance.data))

View file

@ -1,116 +0,0 @@
import pprint
import pyblish.api
from openpype.settings import get_project_settings
from openpype.hosts.photoshop import api as photoshop
from openpype.lib import prepare_template_data
from openpype.pipeline import legacy_io
class CollectInstances(pyblish.api.ContextPlugin):
"""Gather instances by LayerSet and file metadata
Collects publishable instances from file metadata or enhance
already collected by creator (family == "image").
If no image instances are explicitly created, it looks if there is value
in `flatten_subset_template` (configurable in Settings), in that case it
produces flatten image with all visible layers.
Identifier:
id (str): "pyblish.avalon.instance"
"""
label = "Collect Instances"
order = pyblish.api.CollectorOrder
hosts = ["photoshop"]
families_mapping = {
"image": []
}
# configurable in Settings
flatten_subset_template = ""
def process(self, context):
instance_by_layer_id = {}
for instance in context:
if (
instance.data["family"] == "image" and
instance.data.get("members")):
layer_id = str(instance.data["members"][0])
instance_by_layer_id[layer_id] = instance
stub = photoshop.stub()
layer_items = stub.get_layers()
layers_meta = stub.get_layers_metadata()
instance_names = []
all_layer_ids = []
for layer_item in layer_items:
layer_meta_data = stub.read(layer_item, layers_meta)
all_layer_ids.append(layer_item.id)
# Skip layers without metadata.
if layer_meta_data is None:
continue
# Skip containers.
if "container" in layer_meta_data["id"]:
continue
# active might not be in legacy meta
if not layer_meta_data.get("active", True):
continue
instance = instance_by_layer_id.get(str(layer_item.id))
if instance is None:
instance = context.create_instance(layer_meta_data["subset"])
instance.data["layer"] = layer_item
instance.data.update(layer_meta_data)
instance.data["families"] = self.families_mapping[
layer_meta_data["family"]
]
instance.data["publish"] = layer_item.visible
instance_names.append(layer_meta_data["subset"])
# Produce diagnostic message for any graphical
# user interface interested in visualising it.
self.log.info("Found: \"%s\" " % instance.data["name"])
self.log.info("instance: {} ".format(
pprint.pformat(instance.data, indent=4)))
if len(instance_names) != len(set(instance_names)):
self.log.warning("Duplicate instances found. " +
"Remove unwanted via Publisher")
if len(instance_names) == 0 and self.flatten_subset_template:
project_name = context.data["projectEntity"]["name"]
variants = get_project_settings(project_name).get(
"photoshop", {}).get(
"create", {}).get(
"CreateImage", {}).get(
"defaults", [''])
family = "image"
task_name = legacy_io.Session["AVALON_TASK"]
asset_name = context.data["assetEntity"]["name"]
variant = context.data.get("variant") or variants[0]
fill_pairs = {
"variant": variant,
"family": family,
"task": task_name
}
subset = self.flatten_subset_template.format(
**prepare_template_data(fill_pairs))
instance = context.create_instance(subset)
instance.data["family"] = family
instance.data["asset"] = asset_name
instance.data["subset"] = subset
instance.data["ids"] = all_layer_ids
instance.data["families"] = self.families_mapping[family]
instance.data["publish"] = True
self.log.info("flatten instance: {} ".format(instance.data))

View file

@ -14,10 +14,7 @@ from openpype.pipeline.create import get_subset_name
class CollectReview(pyblish.api.ContextPlugin):
"""Gather the active document as review instance.
Triggers once even if no 'image' is published as by defaults it creates
flatten image from a workfile.
"""Adds review to families for instances marked to be reviewable.
"""
label = "Collect Review"
@ -28,25 +25,8 @@ class CollectReview(pyblish.api.ContextPlugin):
publish = True
def process(self, context):
family = "review"
subset = get_subset_name(
family,
context.data.get("variant", ''),
context.data["anatomyData"]["task"]["name"],
context.data["assetEntity"],
context.data["anatomyData"]["project"]["name"],
host_name=context.data["hostName"],
project_settings=context.data["project_settings"]
)
instance = context.create_instance(subset)
instance.data.update({
"subset": subset,
"label": subset,
"name": subset,
"family": family,
"families": [],
"representations": [],
"asset": os.environ["AVALON_ASSET"],
"publish": self.publish
})
for instance in context:
creator_attributes = instance.data["creator_attributes"]
if (creator_attributes.get("mark_for_review") and
"review" not in instance.data["families"]):
instance.data["families"].append("review")

View file

@ -14,50 +14,19 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
default_variant = "Main"
def process(self, context):
existing_instance = None
for instance in context:
if instance.data["family"] == "workfile":
self.log.debug("Workfile instance found, won't create new")
existing_instance = instance
break
file_path = context.data["currentFile"]
_, ext = os.path.splitext(file_path)
staging_dir = os.path.dirname(file_path)
base_name = os.path.basename(file_path)
family = "workfile"
# context.data["variant"] might come only from collect_batch_data
variant = context.data.get("variant") or self.default_variant
subset = get_subset_name(
family,
variant,
context.data["anatomyData"]["task"]["name"],
context.data["assetEntity"],
context.data["anatomyData"]["project"]["name"],
host_name=context.data["hostName"],
project_settings=context.data["project_settings"]
)
file_path = context.data["currentFile"]
staging_dir = os.path.dirname(file_path)
base_name = os.path.basename(file_path)
# Create instance
if existing_instance is None:
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"]
})
else:
instance = existing_instance
# creating representation
_, ext = os.path.splitext(file_path)
instance.data["representations"].append({
"name": ext[1:],
"ext": ext[1:],
"files": base_name,
"stagingDir": staging_dir,
})
# creating representation
_, ext = os.path.splitext(file_path)
instance.data["representations"].append({
"name": ext[1:],
"ext": ext[1:],
"files": base_name,
"stagingDir": staging_dir,
})
return

View file

@ -47,32 +47,42 @@ class ExtractReview(publish.Extractor):
layers = self._get_layers_from_image_instances(instance)
self.log.info("Layers image instance found: {}".format(layers))
repre_name = "jpg"
repre_skeleton = {
"name": repre_name,
"ext": "jpg",
"stagingDir": staging_dir,
"tags": self.jpg_options['tags'],
}
if instance.data["family"] != "review":
# enable creation of review, without this jpg review would clash
# with jpg of the image family
output_name = repre_name
repre_name = "{}_{}".format(repre_name, output_name)
repre_skeleton.update({"name": repre_name,
"outputName": output_name})
if self.make_image_sequence and len(layers) > 1:
self.log.info("Extract layers to image sequence.")
img_list = self._save_sequence_images(staging_dir, layers)
instance.data["representations"].append({
"name": "jpg",
"ext": "jpg",
"files": img_list,
repre_skeleton.update({
"frameStart": 0,
"frameEnd": len(img_list),
"fps": fps,
"stagingDir": staging_dir,
"tags": self.jpg_options['tags'],
"files": img_list,
})
instance.data["representations"].append(repre_skeleton)
processed_img_names = img_list
else:
self.log.info("Extract layers to flatten image.")
img_list = self._save_flatten_image(staging_dir, layers)
instance.data["representations"].append({
"name": "jpg",
"ext": "jpg",
"files": img_list, # cannot be [] for single frame
"stagingDir": staging_dir,
"tags": self.jpg_options['tags']
repre_skeleton.update({
"files": img_list,
})
instance.data["representations"].append(repre_skeleton)
processed_img_names = [img_list]
ffmpeg_path = get_ffmpeg_tool_path("ffmpeg")

View file

@ -10,23 +10,40 @@
}
},
"create": {
"CreateImage": {
"defaults": [
"ImageCreator": {
"enabled": true,
"active_on_create": true,
"mark_for_review": false,
"default_variants": [
"Main"
]
},
"AutoImageCreator": {
"enabled": false,
"active_on_create": true,
"mark_for_review": false,
"default_variant": ""
},
"ReviewCreator": {
"enabled": true,
"active_on_create": true,
"default_variant": ""
},
"WorkfileCreator": {
"enabled": true,
"active_on_create": true,
"default_variant": "Main"
}
},
"publish": {
"CollectColorCodedInstances": {
"enabled": true,
"create_flatten_image": "no",
"flatten_subset_template": "",
"color_code_mapping": []
},
"CollectInstances": {
"flatten_subset_template": ""
},
"CollectReview": {
"publish": true
"enabled": true
},
"CollectVersion": {
"enabled": false

View file

@ -31,16 +31,126 @@
{
"type": "dict",
"collapsible": true,
"key": "CreateImage",
"key": "ImageCreator",
"label": "Create Image",
"checkbox_key": "enabled",
"children": [
{
"type": "label",
"label": "Manually create instance from layer or group of layers. \n Separate review could be created for this image to be sent to Asset Management System."
},
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "active_on_create",
"label": "Active by default"
},
{
"type": "boolean",
"key": "mark_for_review",
"label": "Review by default"
},
{
"type": "list",
"key": "defaults",
"label": "Default Subsets",
"key": "default_variants",
"label": "Default Variants",
"object_type": "text"
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "AutoImageCreator",
"label": "Create Flatten Image",
"checkbox_key": "enabled",
"children": [
{
"type": "label",
"label": "Auto create image for all visible layers, used for simplified processing. \n Separate review could be created for this image to be sent to Asset Management System."
},
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "active_on_create",
"label": "Active by default"
},
{
"type": "boolean",
"key": "mark_for_review",
"label": "Review by default"
},
{
"type": "text",
"key": "default_variant",
"label": "Default variant"
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "ReviewCreator",
"label": "Create Review",
"checkbox_key": "enabled",
"children": [
{
"type": "label",
"label": "Auto create review instance containing all published image instances or visible layers if no image instance."
},
{
"type": "boolean",
"key": "enabled",
"label": "Enabled",
"default": true
},
{
"type": "boolean",
"key": "active_on_create",
"label": "Active by default"
},
{
"type": "text",
"key": "default_variant",
"label": "Default variant"
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "WorkfileCreator",
"label": "Create Workfile",
"checkbox_key": "enabled",
"children": [
{
"type": "label",
"label": "Auto create workfile instance"
},
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "active_on_create",
"label": "Active by default"
},
{
"type": "text",
"key": "default_variant",
"label": "Default variant"
}
]
}
]
},
@ -56,11 +166,18 @@
"is_group": true,
"key": "CollectColorCodedInstances",
"label": "Collect Color Coded Instances",
"checkbox_key": "enabled",
"children": [
{
"type": "label",
"label": "Set color for publishable layers, set its resulting family and template for subset name. \nCan create flatten image from published instances.(Applicable only for remote publishing!)"
},
{
"type": "boolean",
"key": "enabled",
"label": "Enabled",
"default": true
},
{
"key": "create_flatten_image",
"label": "Create flatten image",
@ -131,40 +248,26 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "CollectInstances",
"label": "Collect Instances",
"children": [
{
"type": "label",
"label": "Name for flatten image created if no image instance present"
},
{
"type": "text",
"key": "flatten_subset_template",
"label": "Subset template for flatten image"
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "CollectReview",
"label": "Collect Review",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "publish",
"label": "Active"
}
]
"key": "enabled",
"label": "Enabled",
"default": true
}
]
},
{
"type": "dict",
"key": "CollectVersion",
"label": "Collect Version",
"checkbox_key": "enabled",
"children": [
{
"type": "label",

View file

@ -0,0 +1,93 @@
import logging
from tests.lib.assert_classes import DBAssert
from tests.integration.hosts.photoshop.lib import PhotoshopTestClass
log = logging.getLogger("test_publish_in_photoshop")
class TestPublishInPhotoshopAutoImage(PhotoshopTestClass):
"""Test for publish in Phohoshop with different review configuration.
Workfile contains 3 layers, auto image and review instances created.
Test contains updates to Settings!!!
"""
PERSIST = True
TEST_FILES = [
("1iLF6aNI31qlUCD1rGg9X9eMieZzxL-rc",
"test_photoshop_publish_auto_image.zip", "")
]
APP_GROUP = "photoshop"
# keep empty to locate latest installed variant or explicit
APP_VARIANT = ""
APP_NAME = "{}/{}".format(APP_GROUP, APP_VARIANT)
TIMEOUT = 120 # publish timeout
def test_db_asserts(self, dbcon, publish_finished):
"""Host and input data dependent expected results in DB."""
print("test_db_asserts")
failures = []
failures.append(DBAssert.count_of_types(dbcon, "version", 3))
failures.append(
DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1}))
failures.append(
DBAssert.count_of_types(dbcon, "subset", 0,
name="imageMainForeground"))
failures.append(
DBAssert.count_of_types(dbcon, "subset", 0,
name="imageMainBackground"))
failures.append(
DBAssert.count_of_types(dbcon, "subset", 1,
name="workfileTest_task"))
failures.append(
DBAssert.count_of_types(dbcon, "representation", 5))
additional_args = {"context.subset": "imageMainForeground",
"context.ext": "png"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 0,
additional_args=additional_args))
additional_args = {"context.subset": "imageMainBackground",
"context.ext": "png"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 0,
additional_args=additional_args))
# review from image
additional_args = {"context.subset": "imageBeautyMain",
"context.ext": "jpg",
"name": "jpg_jpg"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "imageBeautyMain",
"context.ext": "jpg",
"name": "jpg"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "review"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
assert not any(failures)
if __name__ == "__main__":
test_case = TestPublishInPhotoshopAutoImage()

View file

@ -0,0 +1,111 @@
import logging
from tests.lib.assert_classes import DBAssert
from tests.integration.hosts.photoshop.lib import PhotoshopTestClass
log = logging.getLogger("test_publish_in_photoshop")
class TestPublishInPhotoshopImageReviews(PhotoshopTestClass):
"""Test for publish in Phohoshop with different review configuration.
Workfile contains 2 image instance, one has review flag, second doesn't.
Regular `review` family is disabled.
Expected result is to `imageMainForeground` to have additional file with
review, `imageMainBackground` without. No separate `review` family.
`test_project_test_asset_imageMainForeground_v001_jpg.jpg` is expected name
of imageForeground review, `_jpg` suffix is needed to differentiate between
image and review file.
"""
PERSIST = True
TEST_FILES = [
("12WGbNy9RJ3m9jlnk0Ib9-IZmONoxIz_p",
"test_photoshop_publish_review.zip", "")
]
APP_GROUP = "photoshop"
# keep empty to locate latest installed variant or explicit
APP_VARIANT = ""
APP_NAME = "{}/{}".format(APP_GROUP, APP_VARIANT)
TIMEOUT = 120 # publish timeout
def test_db_asserts(self, dbcon, publish_finished):
"""Host and input data dependent expected results in DB."""
print("test_db_asserts")
failures = []
failures.append(DBAssert.count_of_types(dbcon, "version", 3))
failures.append(
DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1}))
failures.append(
DBAssert.count_of_types(dbcon, "subset", 1,
name="imageMainForeground"))
failures.append(
DBAssert.count_of_types(dbcon, "subset", 1,
name="imageMainBackground"))
failures.append(
DBAssert.count_of_types(dbcon, "subset", 1,
name="workfileTest_task"))
failures.append(
DBAssert.count_of_types(dbcon, "representation", 6))
additional_args = {"context.subset": "imageMainForeground",
"context.ext": "png"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "imageMainForeground",
"context.ext": "jpg"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 2,
additional_args=additional_args))
additional_args = {"context.subset": "imageMainForeground",
"context.ext": "jpg",
"context.representation": "jpg_jpg"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "imageMainBackground",
"context.ext": "png"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "imageMainBackground",
"context.ext": "jpg"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 1,
additional_args=additional_args))
additional_args = {"context.subset": "imageMainBackground",
"context.ext": "jpg",
"context.representation": "jpg_jpg"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 0,
additional_args=additional_args))
additional_args = {"context.subset": "review"}
failures.append(
DBAssert.count_of_types(dbcon, "representation", 0,
additional_args=additional_args))
assert not any(failures)
if __name__ == "__main__":
test_case = TestPublishInPhotoshopImageReviews()

View file

@ -0,0 +1,127 @@
---
id: admin_hosts_photoshop
title: Photoshop Settings
sidebar_label: Photoshop
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Photoshop settings
There is a couple of settings that could configure publishing process for **Photoshop**.
All of them are Project based, eg. each project could have different configuration.
Location: Settings > Project > Photoshop
![AfterEffects Project Settings](assets/admin_hosts_photoshop_settings.png)
## Color Management (ImageIO)
Placeholder for Color Management. Currently not implemented yet.
## Creator plugins
Contains configurable items for creators used during publishing from Photoshop.
### Create Image
Provides list of [variants](artist_concepts.md#variant) that will be shown to an artist in Publisher. Default value `Main`.
### Create Flatten Image
Provides simplified publishing process. It will create single `image` instance for artist automatically. This instance will
produce flatten image from all visible layers in a workfile.
- Subset template for flatten image - provide template for subset name for this instance (example `imageBeauty`)
- Review - should be separate review created for this instance
### Create Review
Creates single `review` instance automatically. This allows artists to disable it if needed.
### Create Workfile
Creates single `workfile` instance automatically. This allows artists to disable it if needed.
## Publish plugins
Contains configurable items for publish plugins used during publishing from Photoshop.
### Collect Color Coded Instances
Used only in remote publishing!
Allows to create automatically `image` instances for configurable highlight color set on layer or group in the workfile.
#### Create flatten image
- Flatten with images - produce additional `image` with all published `image` instances merged
- Flatten only - produce only merged `image` instance
- No - produce only separate `image` instances
#### Subset template for flatten image
Template used to create subset name automatically (example `image{layer}Main` - uses layer name in subset name)
### Collect Review
Disable if no review should be created
### Collect Version
If enabled it will push version from workfile name to all published items. Eg. if artist is publishing `test_asset_workfile_v005.psd`
produced `image` and `review` files will contain `v005` (even if some previous version were skipped for particular family).
### Validate Containers
Checks if all imported assets to the workfile through `Loader` are in latest version. Limits cases that older version of asset would be used.
If enabled, artist might still decide to disable validation for each publish (for special use cases).
Limit this optionality by toggling `Optional`.
`Active` toggle denotes that by default artists sees that optional validation as enabled.
### Validate naming of subsets and layers
Subset cannot contain invalid characters or extract to file would fail
#### Regex pattern of invalid characters
Contains weird characters like `/`, `/`, these might cause an issue when file (which contains subset name) is created on OS disk.
#### Replacement character
Replace all offending characters with this one. `_` is default.
### Extract Image
Controls extension formats of published instances of `image` family. `png` and `jpg` are by default.
### Extract Review
Controls output definitions of extracted reviews to upload on Asset Management (AM).
#### Makes an image sequence instead of flatten image
If multiple `image` instances are produced, glue created images into image sequence (`mov`) to review all of them separetely.
Without it only flatten image would be produced.
#### Maximum size of sources for review
Set Byte limit for review file. Applicable if gigantic `image` instances are produced, full image size is unnecessary to upload to AM.
#### Extract jpg Options
Handles tags for produced `.jpg` representation. `Create review` and `Add review to Ftrack` are defaults.
#### Extract mov Options
Handles tags for produced `.mov` representation. `Create review` and `Add review to Ftrack` are defaults.
### Workfile Builder
Allows to open prepared workfile for an artist when no workfile exists. Useful to share standards, additional helpful content in the workfile.
Could be configured per `Task type`, eg. `composition` task type could use different `.psd` template file than `art` task.
Workfile template must be accessible for all artists.
(Currently not handled by [SiteSync](module_site_sync.md))

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -126,6 +126,7 @@ module.exports = {
"admin_hosts_nuke",
"admin_hosts_resolve",
"admin_hosts_harmony",
"admin_hosts_photoshop",
"admin_hosts_aftereffects",
"admin_hosts_tvpaint"
],