mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #456 from kalisp/feature/photoshop_with_websocket
photoshop with websocket
This commit is contained in:
commit
00de522a08
13 changed files with 469 additions and 84 deletions
64
pype/modules/websocket_server/hosts/photoshop.py
Normal file
64
pype/modules/websocket_server/hosts/photoshop.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from pype.api import Logger
|
||||
from wsrpc_aiohttp import WebSocketRoute
|
||||
import functools
|
||||
|
||||
import avalon.photoshop as photoshop
|
||||
|
||||
log = Logger().get_logger("WebsocketServer")
|
||||
|
||||
|
||||
class Photoshop(WebSocketRoute):
|
||||
"""
|
||||
One route, mimicking external application (like Harmony, etc).
|
||||
All functions could be called from client.
|
||||
'do_notify' function calls function on the client - mimicking
|
||||
notification after long running job on the server or similar
|
||||
"""
|
||||
instance = None
|
||||
|
||||
def init(self, **kwargs):
|
||||
# Python __init__ must be return "self".
|
||||
# This method might return anything.
|
||||
log.debug("someone called Photoshop route")
|
||||
self.instance = self
|
||||
return kwargs
|
||||
|
||||
# server functions
|
||||
async def ping(self):
|
||||
log.debug("someone called Photoshop route ping")
|
||||
|
||||
# This method calls function on the client side
|
||||
# client functions
|
||||
|
||||
async def read(self):
|
||||
log.debug("photoshop.read client calls server server calls "
|
||||
"Photo client")
|
||||
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")
|
||||
|
||||
async def loader_route(self):
|
||||
self._tool_route("loader")
|
||||
|
||||
async def publish_route(self):
|
||||
self._tool_route("publish")
|
||||
|
||||
async def sceneinventory_route(self):
|
||||
self._tool_route("sceneinventory")
|
||||
|
||||
async def projectmanager_route(self):
|
||||
self._tool_route("projectmanager")
|
||||
|
||||
def _tool_route(self, tool_name):
|
||||
"""The address accessed when clicking on the buttons."""
|
||||
partial_method = functools.partial(photoshop.show, tool_name)
|
||||
|
||||
photoshop.execute_in_main_thread(partial_method)
|
||||
|
||||
# Required return statement.
|
||||
return "nothing"
|
||||
283
pype/modules/websocket_server/stubs/photoshop_server_stub.py
Normal file
283
pype/modules/websocket_server/stubs/photoshop_server_stub.py
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
from pype.modules.websocket_server import WebSocketServer
|
||||
"""
|
||||
Stub handling connection from server to client.
|
||||
Used anywhere solution is calling client methods.
|
||||
"""
|
||||
import json
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class PhotoshopServerStub():
|
||||
"""
|
||||
Stub for calling function on client (Photoshop js) side.
|
||||
Expects that client is already connected (started when avalon menu
|
||||
is opened).
|
||||
'self.websocketserver.call' is used as async wrapper
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.websocketserver = WebSocketServer.get_instance()
|
||||
self.client = self.websocketserver.get_client()
|
||||
|
||||
def open(self, path):
|
||||
"""
|
||||
Open file located at 'path' (local).
|
||||
:param path: <string> file path locally
|
||||
:return: None
|
||||
"""
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.open', path=path)
|
||||
)
|
||||
|
||||
def read(self, layer, layers_meta=None):
|
||||
"""
|
||||
Parses layer metadata from Headline field of active document
|
||||
:param layer: <namedTuple Layer("id":XX, "name":"YYY")
|
||||
:param layers_meta: full list from Headline (for performance in loops)
|
||||
:return:
|
||||
"""
|
||||
if layers_meta is None:
|
||||
layers_meta = self.get_layers_metadata()
|
||||
|
||||
return layers_meta.get(str(layer.id))
|
||||
|
||||
def imprint(self, layer, data, all_layers=None, layers_meta=None):
|
||||
"""
|
||||
Save layer metadata to Headline field of active document
|
||||
:param layer: <namedTuple> Layer("id": XXX, "name":'YYY')
|
||||
:param data: <string> json representation for single layer
|
||||
:param all_layers: <list of namedTuples> - for performance, could be
|
||||
injected for usage in loop, if not, single call will be
|
||||
triggered
|
||||
:param layers_meta: <string> json representation from Headline
|
||||
(for performance - provide only if imprint is in
|
||||
loop - value should be same)
|
||||
:return: 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)
|
||||
else:
|
||||
layers_meta[str(layer.id)] = data
|
||||
|
||||
# Ensure only valid ids are stored.
|
||||
if not all_layers:
|
||||
all_layers = self.get_layers()
|
||||
layer_ids = [layer.id for layer in all_layers]
|
||||
cleaned_data = {}
|
||||
|
||||
for id in layers_meta:
|
||||
if int(id) in layer_ids:
|
||||
cleaned_data[id] = layers_meta[id]
|
||||
|
||||
payload = json.dumps(cleaned_data, indent=4)
|
||||
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.imprint', payload=payload)
|
||||
)
|
||||
|
||||
def get_layers(self):
|
||||
"""
|
||||
Returns JSON document with all(?) layers in active document.
|
||||
|
||||
:return: <list of namedtuples>
|
||||
Format of tuple: { 'id':'123',
|
||||
'name': 'My Layer 1',
|
||||
'type': 'GUIDE'|'FG'|'BG'|'OBJ'
|
||||
'visible': 'true'|'false'
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.get_layers'))
|
||||
|
||||
return self._to_records(res)
|
||||
|
||||
def get_layers_in_layers(self, layers):
|
||||
"""
|
||||
Return all layers that belong to layers (might be groups).
|
||||
:param layers: <list of namedTuples>
|
||||
:return: <list of namedTuples>
|
||||
"""
|
||||
all_layers = self.get_layers()
|
||||
ret = []
|
||||
parent_ids = set([lay.id for lay in layers])
|
||||
|
||||
for layer in all_layers:
|
||||
parents = set(layer.parents)
|
||||
if len(parent_ids & parents) > 0:
|
||||
ret.append(layer)
|
||||
if layer.id in parent_ids:
|
||||
ret.append(layer)
|
||||
|
||||
return ret
|
||||
|
||||
def create_group(self, name):
|
||||
"""
|
||||
Create new group (eg. LayerSet)
|
||||
:return: <namedTuple Layer("id":XX, "name":"YYY")>
|
||||
"""
|
||||
ret = self.websocketserver.call(self.client.call
|
||||
('Photoshop.create_group',
|
||||
name=name))
|
||||
# create group on PS is asynchronous, returns only id
|
||||
layer = {"id": ret, "name": name, "group": True}
|
||||
return namedtuple('Layer', layer.keys())(*layer.values())
|
||||
|
||||
def group_selected_layers(self, name):
|
||||
"""
|
||||
Group selected layers into new LayerSet (eg. group)
|
||||
:return: <json representation of Layer>
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.group_selected_layers',
|
||||
name=name)
|
||||
)
|
||||
return self._to_records(res)
|
||||
|
||||
def get_selected_layers(self):
|
||||
"""
|
||||
Get a list of actually selected layers
|
||||
:return: <list of Layer('id':XX, 'name':"YYY")>
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.get_selected_layers'))
|
||||
return self._to_records(res)
|
||||
|
||||
def select_layers(self, layers):
|
||||
"""
|
||||
Selecte specified layers in Photoshop
|
||||
:param layers: <list of Layer('id':XX, 'name':"YYY")>
|
||||
:return: None
|
||||
"""
|
||||
layer_ids = [layer.id for layer in layers]
|
||||
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.get_layers',
|
||||
layers=layer_ids)
|
||||
)
|
||||
|
||||
def get_active_document_full_name(self):
|
||||
"""
|
||||
Returns full name with path of active document via ws call
|
||||
:return: <string> full path with name
|
||||
"""
|
||||
res = self.websocketserver.call(
|
||||
self.client.call('Photoshop.get_active_document_full_name'))
|
||||
|
||||
return res
|
||||
|
||||
def get_active_document_name(self):
|
||||
"""
|
||||
Returns just a name of active document via ws call
|
||||
:return: <string> file name
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.get_active_document_name'))
|
||||
|
||||
return res
|
||||
|
||||
def is_saved(self):
|
||||
"""
|
||||
Returns true if no changes in active document
|
||||
:return: <boolean>
|
||||
"""
|
||||
return self.websocketserver.call(self.client.call
|
||||
('Photoshop.is_saved'))
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Saves active document
|
||||
:return: None
|
||||
"""
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.save'))
|
||||
|
||||
def saveAs(self, image_path, ext, as_copy):
|
||||
"""
|
||||
Saves active document to psd (copy) or png or jpg
|
||||
:param image_path: <string> full local path
|
||||
:param ext: <string psd|jpg|png>
|
||||
:param as_copy: <boolean>
|
||||
:return: None
|
||||
"""
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.saveAs',
|
||||
image_path=image_path,
|
||||
ext=ext,
|
||||
as_copy=as_copy))
|
||||
|
||||
def set_visible(self, layer_id, visibility):
|
||||
"""
|
||||
Set layer with 'layer_id' to 'visibility'
|
||||
:param layer_id: <int>
|
||||
:param visibility: <true - set visible, false - hide>
|
||||
:return: None
|
||||
"""
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.set_visible',
|
||||
layer_id=layer_id,
|
||||
visibility=visibility))
|
||||
|
||||
def get_layers_metadata(self):
|
||||
"""
|
||||
Reads layers metadata from Headline from active document in PS.
|
||||
(Headline accessible by File > File Info)
|
||||
:return: <string> - json documents
|
||||
"""
|
||||
layers_data = {}
|
||||
res = self.websocketserver.call(self.client.call('Photoshop.read'))
|
||||
try:
|
||||
layers_data = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
return layers_data
|
||||
|
||||
def import_smart_object(self, path):
|
||||
"""
|
||||
Import the file at `path` as a smart object to active document.
|
||||
|
||||
Args:
|
||||
path (str): File path to import.
|
||||
"""
|
||||
res = self.websocketserver.call(self.client.call
|
||||
('Photoshop.import_smart_object',
|
||||
path=path))
|
||||
|
||||
return self._to_records(res).pop()
|
||||
|
||||
def replace_smart_object(self, layer, path):
|
||||
"""
|
||||
Replace the smart object `layer` with file at `path`
|
||||
|
||||
Args:
|
||||
layer (namedTuple): Layer("id":XX, "name":"YY"..).
|
||||
path (str): File to import.
|
||||
"""
|
||||
self.websocketserver.call(self.client.call
|
||||
('Photoshop.replace_smart_object',
|
||||
layer=layer,
|
||||
path=path))
|
||||
|
||||
def close(self):
|
||||
self.client.close()
|
||||
|
||||
def _to_records(self, res):
|
||||
"""
|
||||
Converts string json representation into list of named tuples for
|
||||
dot notation access to work.
|
||||
:return: <list of named tuples>
|
||||
:param res: <string> - json representation
|
||||
"""
|
||||
try:
|
||||
layers_data = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError("Received broken JSON {}".format(res))
|
||||
ret = []
|
||||
# convert to namedtuple to use dot donation
|
||||
if isinstance(layers_data, dict): # TODO refactore
|
||||
layers_data = [layers_data]
|
||||
for d in layers_data:
|
||||
ret.append(namedtuple('Layer', d.keys())(*d.values()))
|
||||
return ret
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from pype.api import config, Logger
|
||||
from pype.api import Logger
|
||||
|
||||
import threading
|
||||
from aiohttp import web
|
||||
|
|
@ -9,6 +9,7 @@ import os
|
|||
import sys
|
||||
import pyclbr
|
||||
import importlib
|
||||
import urllib
|
||||
|
||||
log = Logger().get_logger("WebsocketServer")
|
||||
|
||||
|
|
@ -19,24 +20,23 @@ class WebSocketServer():
|
|||
Uses class in external_app_1.py to mimic implementation for single
|
||||
external application.
|
||||
'test_client' folder contains two test implementations of client
|
||||
|
||||
WIP
|
||||
"""
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
self.qaction = None
|
||||
self.failed_icon = None
|
||||
self._is_running = False
|
||||
default_port = 8099
|
||||
WebSocketServer._instance = self
|
||||
self.client = None
|
||||
self.handlers = {}
|
||||
|
||||
try:
|
||||
self.presets = config.get_presets()["services"]["websocket_server"]
|
||||
except Exception:
|
||||
self.presets = {"default_port": default_port, "exclude_ports": []}
|
||||
log.debug((
|
||||
"There are not set presets for WebsocketServer."
|
||||
" Using defaults \"{}\""
|
||||
).format(str(self.presets)))
|
||||
websocket_url = os.getenv("WEBSOCKET_URL")
|
||||
if websocket_url:
|
||||
parsed = urllib.parse.urlparse(websocket_url)
|
||||
port = parsed.port
|
||||
if not port:
|
||||
port = 8099 # fallback
|
||||
|
||||
self.app = web.Application()
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ class WebSocketServer():
|
|||
directories_with_routes = ['hosts']
|
||||
self.add_routes_for_directories(directories_with_routes)
|
||||
|
||||
self.websocket_thread = WebsocketServerThread(self, default_port)
|
||||
self.websocket_thread = WebsocketServerThread(self, port)
|
||||
|
||||
def add_routes_for_directories(self, directories_with_routes):
|
||||
""" Loops through selected directories to find all modules and
|
||||
|
|
@ -78,6 +78,33 @@ class WebSocketServer():
|
|||
WebSocketAsync.add_route(class_name, cls)
|
||||
sys.path.pop()
|
||||
|
||||
def call(self, func):
|
||||
log.debug("websocket.call {}".format(func))
|
||||
future = asyncio.run_coroutine_threadsafe(func,
|
||||
self.websocket_thread.loop)
|
||||
result = future.result()
|
||||
return result
|
||||
|
||||
def get_client(self):
|
||||
"""
|
||||
Return first connected client to WebSocket
|
||||
TODO implement selection by Route
|
||||
:return: <WebSocketAsync> client
|
||||
"""
|
||||
clients = WebSocketAsync.get_clients()
|
||||
client = None
|
||||
if len(clients) > 0:
|
||||
key = list(clients.keys())[0]
|
||||
client = clients.get(key)
|
||||
|
||||
return client
|
||||
|
||||
@staticmethod
|
||||
def get_instance():
|
||||
if WebSocketServer._instance is None:
|
||||
WebSocketServer()
|
||||
return WebSocketServer._instance
|
||||
|
||||
def tray_start(self):
|
||||
self.websocket_thread.start()
|
||||
|
||||
|
|
@ -124,6 +151,7 @@ class WebsocketServerThread(threading.Thread):
|
|||
self.loop = None
|
||||
self.runner = None
|
||||
self.site = None
|
||||
self.tasks = []
|
||||
|
||||
def run(self):
|
||||
self.is_running = True
|
||||
|
|
@ -169,6 +197,12 @@ class WebsocketServerThread(threading.Thread):
|
|||
periodically.
|
||||
"""
|
||||
while self.is_running:
|
||||
while self.tasks:
|
||||
task = self.tasks.pop(0)
|
||||
log.debug("waiting for task {}".format(task))
|
||||
await task
|
||||
log.debug("returned value {}".format(task.result))
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
log.debug("Starting shutdown")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from avalon import api, photoshop
|
||||
from avalon import api
|
||||
from avalon.vendor import Qt
|
||||
from avalon import photoshop
|
||||
|
||||
|
||||
class CreateImage(api.Creator):
|
||||
|
|
@ -13,11 +14,12 @@ class CreateImage(api.Creator):
|
|||
groups = []
|
||||
layers = []
|
||||
create_group = False
|
||||
group_constant = photoshop.get_com_objects().constants().psLayerSet
|
||||
|
||||
stub = photoshop.stub()
|
||||
if (self.options or {}).get("useSelection"):
|
||||
multiple_instances = False
|
||||
selection = photoshop.get_selected_layers()
|
||||
|
||||
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.
|
||||
|
|
@ -40,19 +42,18 @@ class CreateImage(api.Creator):
|
|||
|
||||
if multiple_instances:
|
||||
for item in selection:
|
||||
if item.LayerType == group_constant:
|
||||
if item.group:
|
||||
groups.append(item)
|
||||
else:
|
||||
layers.append(item)
|
||||
else:
|
||||
group = photoshop.group_selected_layers()
|
||||
group.Name = self.name
|
||||
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].LayerType == group_constant:
|
||||
if selection[0].group:
|
||||
groups.append(selection[0])
|
||||
else:
|
||||
layers.append(selection[0])
|
||||
|
|
@ -63,16 +64,14 @@ class CreateImage(api.Creator):
|
|||
create_group = True
|
||||
|
||||
if create_group:
|
||||
group = photoshop.app().ActiveDocument.LayerSets.Add()
|
||||
group.Name = self.name
|
||||
group = stub.create_group(self.name)
|
||||
groups.append(group)
|
||||
|
||||
for layer in layers:
|
||||
photoshop.select_layers([layer])
|
||||
group = photoshop.group_selected_layers()
|
||||
group.Name = layer.Name
|
||||
stub.select_layers([layer])
|
||||
group = stub.group_selected_layers(layer.name)
|
||||
groups.append(group)
|
||||
|
||||
for group in groups:
|
||||
self.data.update({"subset": "image" + group.Name})
|
||||
photoshop.imprint(group, self.data)
|
||||
self.data.update({"subset": "image" + group.name})
|
||||
stub.imprint(group, self.data)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from avalon import api, photoshop
|
||||
|
||||
stub = photoshop.stub()
|
||||
|
||||
|
||||
class ImageLoader(api.Loader):
|
||||
"""Load images
|
||||
|
|
@ -12,7 +14,7 @@ class ImageLoader(api.Loader):
|
|||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
with photoshop.maintained_selection():
|
||||
layer = photoshop.import_smart_object(self.fname)
|
||||
layer = stub.import_smart_object(self.fname)
|
||||
|
||||
self[:] = [layer]
|
||||
|
||||
|
|
@ -28,11 +30,11 @@ class ImageLoader(api.Loader):
|
|||
layer = container.pop("layer")
|
||||
|
||||
with photoshop.maintained_selection():
|
||||
photoshop.replace_smart_object(
|
||||
stub.replace_smart_object(
|
||||
layer, api.get_representation_path(representation)
|
||||
)
|
||||
|
||||
photoshop.imprint(
|
||||
stub.imprint(
|
||||
layer, {"representation": str(representation["_id"])}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from avalon import photoshop
|
||||
|
||||
|
||||
|
|
@ -13,5 +14,5 @@ class CollectCurrentFile(pyblish.api.ContextPlugin):
|
|||
|
||||
def process(self, context):
|
||||
context.data["currentFile"] = os.path.normpath(
|
||||
photoshop.app().ActiveDocument.FullName
|
||||
photoshop.stub().get_active_document_full_name()
|
||||
).replace("\\", "/")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import pythoncom
|
||||
|
||||
from avalon import photoshop
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from avalon import photoshop
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
"""Gather instances by LayerSet and file metadata
|
||||
|
|
@ -27,8 +27,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
# can be.
|
||||
pythoncom.CoInitialize()
|
||||
|
||||
for layer in photoshop.get_layers_in_document():
|
||||
layer_data = photoshop.read(layer)
|
||||
stub = photoshop.stub()
|
||||
layers = stub.get_layers()
|
||||
layers_meta = stub.get_layers_metadata()
|
||||
for layer in layers:
|
||||
layer_data = stub.read(layer, layers_meta)
|
||||
|
||||
# Skip layers without metadata.
|
||||
if layer_data is None:
|
||||
|
|
@ -38,18 +41,19 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
if "container" in layer_data["id"]:
|
||||
continue
|
||||
|
||||
child_layers = [*layer.Layers]
|
||||
if not child_layers:
|
||||
self.log.info("%s skipped, it was empty." % layer.Name)
|
||||
continue
|
||||
# child_layers = [*layer.Layers]
|
||||
# self.log.debug("child_layers {}".format(child_layers))
|
||||
# if not child_layers:
|
||||
# self.log.info("%s skipped, it was empty." % layer.Name)
|
||||
# continue
|
||||
|
||||
instance = context.create_instance(layer.Name)
|
||||
instance = context.create_instance(layer.name)
|
||||
instance.append(layer)
|
||||
instance.data.update(layer_data)
|
||||
instance.data["families"] = self.families_mapping[
|
||||
layer_data["family"]
|
||||
]
|
||||
instance.data["publish"] = layer.Visible
|
||||
instance.data["publish"] = layer.visible
|
||||
|
||||
# Produce diagnostic message for any graphical
|
||||
# user interface interested in visualising it.
|
||||
|
|
|
|||
|
|
@ -21,35 +21,37 @@ class ExtractImage(pype.api.Extractor):
|
|||
self.log.info("Outputting image to {}".format(staging_dir))
|
||||
|
||||
# Perform extraction
|
||||
stub = photoshop.stub()
|
||||
files = {}
|
||||
with photoshop.maintained_selection():
|
||||
self.log.info("Extracting %s" % str(list(instance)))
|
||||
with photoshop.maintained_visibility():
|
||||
# Hide all other layers.
|
||||
extract_ids = [
|
||||
x.id for x in photoshop.get_layers_in_layers([instance[0]])
|
||||
]
|
||||
for layer in photoshop.get_layers_in_document():
|
||||
if layer.id not in extract_ids:
|
||||
layer.Visible = False
|
||||
extract_ids = set([ll.id for ll in stub.
|
||||
get_layers_in_layers([instance[0]])])
|
||||
|
||||
save_options = {}
|
||||
for layer in stub.get_layers():
|
||||
# limit unnecessary calls to client
|
||||
if layer.visible and layer.id not in extract_ids:
|
||||
stub.set_visible(layer.id, False)
|
||||
if not layer.visible and layer.id in extract_ids:
|
||||
stub.set_visible(layer.id, True)
|
||||
|
||||
save_options = []
|
||||
if "png" in self.formats:
|
||||
save_options["png"] = photoshop.com_objects.PNGSaveOptions()
|
||||
save_options.append('png')
|
||||
if "jpg" in self.formats:
|
||||
save_options["jpg"] = photoshop.com_objects.JPEGSaveOptions()
|
||||
save_options.append('jpg')
|
||||
|
||||
file_basename = os.path.splitext(
|
||||
photoshop.app().ActiveDocument.Name
|
||||
stub.get_active_document_name()
|
||||
)[0]
|
||||
for extension, save_option in save_options.items():
|
||||
for extension in save_options:
|
||||
_filename = "{}.{}".format(file_basename, extension)
|
||||
files[extension] = _filename
|
||||
|
||||
full_filename = os.path.join(staging_dir, _filename)
|
||||
photoshop.app().ActiveDocument.SaveAs(
|
||||
full_filename, save_option, True
|
||||
)
|
||||
stub.saveAs(full_filename, extension, True)
|
||||
|
||||
representations = []
|
||||
for extension, filename in files.items():
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@ class ExtractReview(pype.api.Extractor):
|
|||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
staging_dir = self.staging_dir(instance)
|
||||
self.log.info("Outputting image to {}".format(staging_dir))
|
||||
|
||||
stub = photoshop.stub()
|
||||
|
||||
layers = []
|
||||
for image_instance in instance.context:
|
||||
if image_instance.data["family"] != "image":
|
||||
|
|
@ -25,25 +26,22 @@ class ExtractReview(pype.api.Extractor):
|
|||
|
||||
# Perform extraction
|
||||
output_image = "{}.jpg".format(
|
||||
os.path.splitext(photoshop.app().ActiveDocument.Name)[0]
|
||||
os.path.splitext(stub.get_active_document_name())[0]
|
||||
)
|
||||
output_image_path = os.path.join(staging_dir, output_image)
|
||||
with photoshop.maintained_visibility():
|
||||
# Hide all other layers.
|
||||
extract_ids = [
|
||||
x.id for x in photoshop.get_layers_in_layers(layers)
|
||||
]
|
||||
for layer in photoshop.get_layers_in_document():
|
||||
if layer.id in extract_ids:
|
||||
layer.Visible = True
|
||||
else:
|
||||
layer.Visible = False
|
||||
extract_ids = set([ll.id for ll in stub.
|
||||
get_layers_in_layers(layers)])
|
||||
self.log.info("extract_ids {}".format(extract_ids))
|
||||
for layer in stub.get_layers():
|
||||
# limit unnecessary calls to client
|
||||
if layer.visible and layer.id not in extract_ids:
|
||||
stub.set_visible(layer.id, False)
|
||||
if not layer.visible and layer.id in extract_ids:
|
||||
stub.set_visible(layer.id, True)
|
||||
|
||||
photoshop.app().ActiveDocument.SaveAs(
|
||||
output_image_path,
|
||||
photoshop.com_objects.JPEGSaveOptions(),
|
||||
True
|
||||
)
|
||||
stub.saveAs(output_image_path, 'jpg', True)
|
||||
|
||||
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||
|
||||
|
|
@ -66,8 +64,6 @@ class ExtractReview(pype.api.Extractor):
|
|||
]
|
||||
output = pype.lib._subprocess(args)
|
||||
|
||||
self.log.debug(output)
|
||||
|
||||
instance.data["representations"].append({
|
||||
"name": "thumbnail",
|
||||
"ext": "jpg",
|
||||
|
|
@ -75,7 +71,6 @@ class ExtractReview(pype.api.Extractor):
|
|||
"stagingDir": staging_dir,
|
||||
"tags": ["thumbnail"]
|
||||
})
|
||||
|
||||
# Generate mov.
|
||||
mov_path = os.path.join(staging_dir, "review.mov")
|
||||
args = [
|
||||
|
|
@ -86,9 +81,7 @@ class ExtractReview(pype.api.Extractor):
|
|||
mov_path
|
||||
]
|
||||
output = pype.lib._subprocess(args)
|
||||
|
||||
self.log.debug(output)
|
||||
|
||||
instance.data["representations"].append({
|
||||
"name": "mov",
|
||||
"ext": "mov",
|
||||
|
|
|
|||
|
|
@ -11,4 +11,4 @@ class ExtractSaveScene(pype.api.Extractor):
|
|||
families = ["workfile"]
|
||||
|
||||
def process(self, instance):
|
||||
photoshop.app().ActiveDocument.Save()
|
||||
photoshop.stub().save()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import pyblish.api
|
||||
from pype.action import get_errored_plugins_from_data
|
||||
from pype.lib import version_up
|
||||
|
||||
from avalon import photoshop
|
||||
|
||||
|
||||
|
|
@ -24,6 +25,6 @@ class IncrementWorkfile(pyblish.api.InstancePlugin):
|
|||
)
|
||||
|
||||
scene_path = version_up(instance.context.data["currentFile"])
|
||||
photoshop.app().ActiveDocument.SaveAs(scene_path)
|
||||
photoshop.stub().saveAs(scene_path, 'psd', True)
|
||||
|
||||
self.log.info("Incremented workfile to: {}".format(scene_path))
|
||||
|
|
|
|||
|
|
@ -23,11 +23,12 @@ class ValidateInstanceAssetRepair(pyblish.api.Action):
|
|||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
|
||||
stub = photoshop.stub()
|
||||
for instance in instances:
|
||||
data = photoshop.read(instance[0])
|
||||
data = stub.read(instance[0])
|
||||
|
||||
data["asset"] = os.environ["AVALON_ASSET"]
|
||||
photoshop.imprint(instance[0], data)
|
||||
stub.imprint(instance[0], data)
|
||||
|
||||
|
||||
class ValidateInstanceAsset(pyblish.api.InstancePlugin):
|
||||
|
|
|
|||
|
|
@ -21,13 +21,14 @@ class ValidateNamingRepair(pyblish.api.Action):
|
|||
|
||||
# Apply pyblish.logic to get the instances for the plug-in
|
||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||
|
||||
stub = photoshop.stub()
|
||||
for instance in instances:
|
||||
self.log.info("validate_naming instance {}".format(instance))
|
||||
name = instance.data["name"].replace(" ", "_")
|
||||
instance[0].Name = name
|
||||
data = photoshop.read(instance[0])
|
||||
data = stub.read(instance[0])
|
||||
data["subset"] = "image" + name
|
||||
photoshop.imprint(instance[0], data)
|
||||
stub.imprint(instance[0], data)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue