diff --git a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py index e3eda7dd61..8b1c50072e 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail_from_source.py @@ -13,7 +13,9 @@ Todos: """ import os +from dataclasses import dataclass, field, fields import tempfile +from typing import Dict, Any, List, Tuple import pyblish.api from ayon_core.lib import ( @@ -22,9 +24,57 @@ from ayon_core.lib import ( is_oiio_supported, run_subprocess, + get_rescaled_command_arguments, + filter_profiles, ) +@dataclass +class ProfileConfig: + """ + Data class representing the full configuration for selected profile + + Any change of controllable fields in Settings must propagate here! + """ + integrate_thumbnail: bool = False + + target_size: Dict[str, Any] = field( + default_factory=lambda: { + "type": "source", + "resize": {"width": 1920, "height": 1080}, + } + ) + + ffmpeg_args: Dict[str, List[Any]] = field( + default_factory=lambda: {"input": [], "output": []} + ) + + # Background color defined as (R, G, B, A) tuple. + # Note: Use float for alpha channel (0.0 to 1.0). + background_color: Tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ProfileConfig": + """ + Creates a ProfileConfig instance from a dictionary, safely ignoring + any keys in the dictionary that are not fields in the dataclass. + + Args: + data (Dict[str, Any]): The dictionary containing configuration data + + Returns: + MediaConfig: A new instance of the dataclass. + """ + # Get all field names defined in the dataclass + field_names = {f.name for f in fields(cls)} + + # Filter the input dictionary to include only keys that match field names + filtered_data = {k: v for k, v in data.items() if k in field_names} + + # Unpack the filtered dictionary into the constructor + return cls(**filtered_data) + + class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): """Create jpg thumbnail for instance based on 'thumbnailSource'. @@ -56,7 +106,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return dst_filepath = self._create_thumbnail( - instance.context, thumbnail_source + instance.context, thumbnail_source, profile_config ) if not dst_filepath: return @@ -79,7 +129,12 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): instance.data["representations"].append(new_repre) instance.data["thumbnailPath"] = dst_filepath - def _create_thumbnail(self, context, thumbnail_source): + def _create_thumbnail( + self, + context: pyblish.api.Context, + thumbnail_source: str, + profile_config: ProfileConfig + ) -> str: if not thumbnail_source: self.log.debug("Thumbnail source not filled. Skipping.") return @@ -112,7 +167,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): # If the input can read by OIIO then use OIIO method for # conversion otherwise use ffmpeg thumbnail_created = self.create_thumbnail_oiio( - thumbnail_source, full_output_path + thumbnail_source, full_output_path, profile_config ) # Try to use FFMPEG if OIIO is not supported or for cases when @@ -125,7 +180,7 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) thumbnail_created = self.create_thumbnail_ffmpeg( - thumbnail_source, full_output_path + thumbnail_source, full_output_path, profile_config ) # Skip representation and try next one if wasn't created @@ -146,13 +201,15 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): return True return False - def create_thumbnail_oiio(self, src_path, dst_path): + def create_thumbnail_oiio( + self, + src_path: str, + dst_path: str, + profile_config: ProfileConfig + ) -> bool: self.log.debug("Outputting thumbnail with OIIO: {}".format(dst_path)) - oiio_cmd = get_oiio_tool_args( - "oiiotool", - "-a", src_path, - "--ch", "R,G,B", - "-o", dst_path + resolution_arg = self._get_resolution_arg( + "oiiotool", src_path, profile_config ) self.log.debug("Running: {}".format(" ".join(oiio_cmd))) try: @@ -165,7 +222,16 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) return False - def create_thumbnail_ffmpeg(self, src_path, dst_path): + def create_thumbnail_ffmpeg( + self, + src_path: str, + dst_path: str, + profile_config: ProfileConfig + ) -> bool: + resolution_arg = self._get_resolution_arg( + "ffmpeg", src_path, profile_config + ) + max_int = str(2147483647) ffmpeg_cmd = get_ffmpeg_tool_args( "ffmpeg", @@ -188,10 +254,78 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): ) return False - def _create_context_thumbnail(self, context): - if "thumbnailPath" in context.data: + def _create_context_thumbnail( + self, + context: pyblish.api.Context, + profile: ProfileConfig + ) -> str: + hasContextThumbnail = "thumbnailPath" in context.data + if hasContextThumbnail: return thumbnail_source = context.data.get("thumbnailSource") - thumbnail_path = self._create_thumbnail(context, thumbnail_source) - context.data["thumbnailPath"] = thumbnail_path + thumbnail_path = self._create_thumbnail( + context, thumbnail_source, profile + ) + return thumbnail_path + + def _get_config_from_profile( + self, + instance: pyblish.api.Instance + ) -> ProfileConfig: + """Returns profile if and how repre should be color transcoded.""" + host_name = instance.context.data["hostName"] + product_type = instance.data["productType"] + product_name = instance.data["productName"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + filtering_criteria = { + "hosts": host_name, + "product_types": product_type, + "product_names": product_name, + "task_names": task_name, + "task_types": task_type, + } + profile = filter_profiles( + self.profiles, filtering_criteria, + logger=self.log + ) + + if not profile: + self.log.debug( + ( + "Skipped instance. None of profiles in presets are for" + ' Host: "{}" | Product types: "{}" | Product names: "{}"' + ' | Task name "{}" | Task type "{}"' + ).format( + host_name, product_type, product_name, task_name, task_type + ) + ) + return + + return ProfileConfig.from_dict(profile) + + def _get_resolution_arg( + self, + application, + input_path, + profile + ): + # get settings + if profile.target_size["type"] == "source": + return [] + + resize = profile.target_size["resize"] + target_width = resize["width"] + target_height = resize["height"] + + # form arg string per application + return get_rescaled_command_arguments( + application, + input_path, + target_width, + target_height, + bg_color=profile.background_color, + log=self.log, + )