mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 08:24:53 +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
|
import threading
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
@ -9,6 +9,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import pyclbr
|
import pyclbr
|
||||||
import importlib
|
import importlib
|
||||||
|
import urllib
|
||||||
|
|
||||||
log = Logger().get_logger("WebsocketServer")
|
log = Logger().get_logger("WebsocketServer")
|
||||||
|
|
||||||
|
|
@ -19,24 +20,23 @@ class WebSocketServer():
|
||||||
Uses class in external_app_1.py to mimic implementation for single
|
Uses class in external_app_1.py to mimic implementation for single
|
||||||
external application.
|
external application.
|
||||||
'test_client' folder contains two test implementations of client
|
'test_client' folder contains two test implementations of client
|
||||||
|
|
||||||
WIP
|
|
||||||
"""
|
"""
|
||||||
|
_instance = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.qaction = None
|
self.qaction = None
|
||||||
self.failed_icon = None
|
self.failed_icon = None
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
default_port = 8099
|
WebSocketServer._instance = self
|
||||||
|
self.client = None
|
||||||
|
self.handlers = {}
|
||||||
|
|
||||||
try:
|
websocket_url = os.getenv("WEBSOCKET_URL")
|
||||||
self.presets = config.get_presets()["services"]["websocket_server"]
|
if websocket_url:
|
||||||
except Exception:
|
parsed = urllib.parse.urlparse(websocket_url)
|
||||||
self.presets = {"default_port": default_port, "exclude_ports": []}
|
port = parsed.port
|
||||||
log.debug((
|
if not port:
|
||||||
"There are not set presets for WebsocketServer."
|
port = 8099 # fallback
|
||||||
" Using defaults \"{}\""
|
|
||||||
).format(str(self.presets)))
|
|
||||||
|
|
||||||
self.app = web.Application()
|
self.app = web.Application()
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ class WebSocketServer():
|
||||||
directories_with_routes = ['hosts']
|
directories_with_routes = ['hosts']
|
||||||
self.add_routes_for_directories(directories_with_routes)
|
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):
|
def add_routes_for_directories(self, directories_with_routes):
|
||||||
""" Loops through selected directories to find all modules and
|
""" Loops through selected directories to find all modules and
|
||||||
|
|
@ -78,6 +78,33 @@ class WebSocketServer():
|
||||||
WebSocketAsync.add_route(class_name, cls)
|
WebSocketAsync.add_route(class_name, cls)
|
||||||
sys.path.pop()
|
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):
|
def tray_start(self):
|
||||||
self.websocket_thread.start()
|
self.websocket_thread.start()
|
||||||
|
|
||||||
|
|
@ -124,6 +151,7 @@ class WebsocketServerThread(threading.Thread):
|
||||||
self.loop = None
|
self.loop = None
|
||||||
self.runner = None
|
self.runner = None
|
||||||
self.site = None
|
self.site = None
|
||||||
|
self.tasks = []
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
|
@ -169,6 +197,12 @@ class WebsocketServerThread(threading.Thread):
|
||||||
periodically.
|
periodically.
|
||||||
"""
|
"""
|
||||||
while self.is_running:
|
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)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
log.debug("Starting shutdown")
|
log.debug("Starting shutdown")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from avalon import api, photoshop
|
from avalon import api
|
||||||
from avalon.vendor import Qt
|
from avalon.vendor import Qt
|
||||||
|
from avalon import photoshop
|
||||||
|
|
||||||
|
|
||||||
class CreateImage(api.Creator):
|
class CreateImage(api.Creator):
|
||||||
|
|
@ -13,11 +14,12 @@ class CreateImage(api.Creator):
|
||||||
groups = []
|
groups = []
|
||||||
layers = []
|
layers = []
|
||||||
create_group = False
|
create_group = False
|
||||||
group_constant = photoshop.get_com_objects().constants().psLayerSet
|
|
||||||
|
stub = photoshop.stub()
|
||||||
if (self.options or {}).get("useSelection"):
|
if (self.options or {}).get("useSelection"):
|
||||||
multiple_instances = False
|
multiple_instances = False
|
||||||
selection = photoshop.get_selected_layers()
|
selection = stub.get_selected_layers()
|
||||||
|
self.log.info("selection {}".format(selection))
|
||||||
if len(selection) > 1:
|
if len(selection) > 1:
|
||||||
# Ask user whether to create one image or image per selected
|
# Ask user whether to create one image or image per selected
|
||||||
# item.
|
# item.
|
||||||
|
|
@ -40,19 +42,18 @@ class CreateImage(api.Creator):
|
||||||
|
|
||||||
if multiple_instances:
|
if multiple_instances:
|
||||||
for item in selection:
|
for item in selection:
|
||||||
if item.LayerType == group_constant:
|
if item.group:
|
||||||
groups.append(item)
|
groups.append(item)
|
||||||
else:
|
else:
|
||||||
layers.append(item)
|
layers.append(item)
|
||||||
else:
|
else:
|
||||||
group = photoshop.group_selected_layers()
|
group = stub.group_selected_layers(self.name)
|
||||||
group.Name = self.name
|
|
||||||
groups.append(group)
|
groups.append(group)
|
||||||
|
|
||||||
elif len(selection) == 1:
|
elif len(selection) == 1:
|
||||||
# One selected item. Use group if its a LayerSet (group), else
|
# One selected item. Use group if its a LayerSet (group), else
|
||||||
# create a new group.
|
# create a new group.
|
||||||
if selection[0].LayerType == group_constant:
|
if selection[0].group:
|
||||||
groups.append(selection[0])
|
groups.append(selection[0])
|
||||||
else:
|
else:
|
||||||
layers.append(selection[0])
|
layers.append(selection[0])
|
||||||
|
|
@ -63,16 +64,14 @@ class CreateImage(api.Creator):
|
||||||
create_group = True
|
create_group = True
|
||||||
|
|
||||||
if create_group:
|
if create_group:
|
||||||
group = photoshop.app().ActiveDocument.LayerSets.Add()
|
group = stub.create_group(self.name)
|
||||||
group.Name = self.name
|
|
||||||
groups.append(group)
|
groups.append(group)
|
||||||
|
|
||||||
for layer in layers:
|
for layer in layers:
|
||||||
photoshop.select_layers([layer])
|
stub.select_layers([layer])
|
||||||
group = photoshop.group_selected_layers()
|
group = stub.group_selected_layers(layer.name)
|
||||||
group.Name = layer.Name
|
|
||||||
groups.append(group)
|
groups.append(group)
|
||||||
|
|
||||||
for group in groups:
|
for group in groups:
|
||||||
self.data.update({"subset": "image" + group.Name})
|
self.data.update({"subset": "image" + group.name})
|
||||||
photoshop.imprint(group, self.data)
|
stub.imprint(group, self.data)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from avalon import api, photoshop
|
from avalon import api, photoshop
|
||||||
|
|
||||||
|
stub = photoshop.stub()
|
||||||
|
|
||||||
|
|
||||||
class ImageLoader(api.Loader):
|
class ImageLoader(api.Loader):
|
||||||
"""Load images
|
"""Load images
|
||||||
|
|
@ -12,7 +14,7 @@ class ImageLoader(api.Loader):
|
||||||
|
|
||||||
def load(self, context, name=None, namespace=None, data=None):
|
def load(self, context, name=None, namespace=None, data=None):
|
||||||
with photoshop.maintained_selection():
|
with photoshop.maintained_selection():
|
||||||
layer = photoshop.import_smart_object(self.fname)
|
layer = stub.import_smart_object(self.fname)
|
||||||
|
|
||||||
self[:] = [layer]
|
self[:] = [layer]
|
||||||
|
|
||||||
|
|
@ -28,11 +30,11 @@ class ImageLoader(api.Loader):
|
||||||
layer = container.pop("layer")
|
layer = container.pop("layer")
|
||||||
|
|
||||||
with photoshop.maintained_selection():
|
with photoshop.maintained_selection():
|
||||||
photoshop.replace_smart_object(
|
stub.replace_smart_object(
|
||||||
layer, api.get_representation_path(representation)
|
layer, api.get_representation_path(representation)
|
||||||
)
|
)
|
||||||
|
|
||||||
photoshop.imprint(
|
stub.imprint(
|
||||||
layer, {"representation": str(representation["_id"])}
|
layer, {"representation": str(representation["_id"])}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
from avalon import photoshop
|
from avalon import photoshop
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,5 +14,5 @@ class CollectCurrentFile(pyblish.api.ContextPlugin):
|
||||||
|
|
||||||
def process(self, context):
|
def process(self, context):
|
||||||
context.data["currentFile"] = os.path.normpath(
|
context.data["currentFile"] = os.path.normpath(
|
||||||
photoshop.app().ActiveDocument.FullName
|
photoshop.stub().get_active_document_full_name()
|
||||||
).replace("\\", "/")
|
).replace("\\", "/")
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import pythoncom
|
import pythoncom
|
||||||
|
|
||||||
from avalon import photoshop
|
|
||||||
|
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
|
|
||||||
|
from avalon import photoshop
|
||||||
|
|
||||||
|
|
||||||
class CollectInstances(pyblish.api.ContextPlugin):
|
class CollectInstances(pyblish.api.ContextPlugin):
|
||||||
"""Gather instances by LayerSet and file metadata
|
"""Gather instances by LayerSet and file metadata
|
||||||
|
|
@ -27,8 +27,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
||||||
# can be.
|
# can be.
|
||||||
pythoncom.CoInitialize()
|
pythoncom.CoInitialize()
|
||||||
|
|
||||||
for layer in photoshop.get_layers_in_document():
|
stub = photoshop.stub()
|
||||||
layer_data = photoshop.read(layer)
|
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.
|
# Skip layers without metadata.
|
||||||
if layer_data is None:
|
if layer_data is None:
|
||||||
|
|
@ -38,18 +41,19 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
||||||
if "container" in layer_data["id"]:
|
if "container" in layer_data["id"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
child_layers = [*layer.Layers]
|
# child_layers = [*layer.Layers]
|
||||||
if not child_layers:
|
# self.log.debug("child_layers {}".format(child_layers))
|
||||||
self.log.info("%s skipped, it was empty." % layer.Name)
|
# if not child_layers:
|
||||||
continue
|
# 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.append(layer)
|
||||||
instance.data.update(layer_data)
|
instance.data.update(layer_data)
|
||||||
instance.data["families"] = self.families_mapping[
|
instance.data["families"] = self.families_mapping[
|
||||||
layer_data["family"]
|
layer_data["family"]
|
||||||
]
|
]
|
||||||
instance.data["publish"] = layer.Visible
|
instance.data["publish"] = layer.visible
|
||||||
|
|
||||||
# Produce diagnostic message for any graphical
|
# Produce diagnostic message for any graphical
|
||||||
# user interface interested in visualising it.
|
# user interface interested in visualising it.
|
||||||
|
|
|
||||||
|
|
@ -21,35 +21,37 @@ class ExtractImage(pype.api.Extractor):
|
||||||
self.log.info("Outputting image to {}".format(staging_dir))
|
self.log.info("Outputting image to {}".format(staging_dir))
|
||||||
|
|
||||||
# Perform extraction
|
# Perform extraction
|
||||||
|
stub = photoshop.stub()
|
||||||
files = {}
|
files = {}
|
||||||
with photoshop.maintained_selection():
|
with photoshop.maintained_selection():
|
||||||
self.log.info("Extracting %s" % str(list(instance)))
|
self.log.info("Extracting %s" % str(list(instance)))
|
||||||
with photoshop.maintained_visibility():
|
with photoshop.maintained_visibility():
|
||||||
# Hide all other layers.
|
# Hide all other layers.
|
||||||
extract_ids = [
|
extract_ids = set([ll.id for ll in stub.
|
||||||
x.id for x in photoshop.get_layers_in_layers([instance[0]])
|
get_layers_in_layers([instance[0]])])
|
||||||
]
|
|
||||||
for layer in photoshop.get_layers_in_document():
|
|
||||||
if layer.id not in extract_ids:
|
|
||||||
layer.Visible = False
|
|
||||||
|
|
||||||
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:
|
if "png" in self.formats:
|
||||||
save_options["png"] = photoshop.com_objects.PNGSaveOptions()
|
save_options.append('png')
|
||||||
if "jpg" in self.formats:
|
if "jpg" in self.formats:
|
||||||
save_options["jpg"] = photoshop.com_objects.JPEGSaveOptions()
|
save_options.append('jpg')
|
||||||
|
|
||||||
file_basename = os.path.splitext(
|
file_basename = os.path.splitext(
|
||||||
photoshop.app().ActiveDocument.Name
|
stub.get_active_document_name()
|
||||||
)[0]
|
)[0]
|
||||||
for extension, save_option in save_options.items():
|
for extension in save_options:
|
||||||
_filename = "{}.{}".format(file_basename, extension)
|
_filename = "{}.{}".format(file_basename, extension)
|
||||||
files[extension] = _filename
|
files[extension] = _filename
|
||||||
|
|
||||||
full_filename = os.path.join(staging_dir, _filename)
|
full_filename = os.path.join(staging_dir, _filename)
|
||||||
photoshop.app().ActiveDocument.SaveAs(
|
stub.saveAs(full_filename, extension, True)
|
||||||
full_filename, save_option, True
|
|
||||||
)
|
|
||||||
|
|
||||||
representations = []
|
representations = []
|
||||||
for extension, filename in files.items():
|
for extension, filename in files.items():
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@ class ExtractReview(pype.api.Extractor):
|
||||||
families = ["review"]
|
families = ["review"]
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
|
|
||||||
staging_dir = self.staging_dir(instance)
|
staging_dir = self.staging_dir(instance)
|
||||||
self.log.info("Outputting image to {}".format(staging_dir))
|
self.log.info("Outputting image to {}".format(staging_dir))
|
||||||
|
|
||||||
|
stub = photoshop.stub()
|
||||||
|
|
||||||
layers = []
|
layers = []
|
||||||
for image_instance in instance.context:
|
for image_instance in instance.context:
|
||||||
if image_instance.data["family"] != "image":
|
if image_instance.data["family"] != "image":
|
||||||
|
|
@ -25,25 +26,22 @@ class ExtractReview(pype.api.Extractor):
|
||||||
|
|
||||||
# Perform extraction
|
# Perform extraction
|
||||||
output_image = "{}.jpg".format(
|
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)
|
output_image_path = os.path.join(staging_dir, output_image)
|
||||||
with photoshop.maintained_visibility():
|
with photoshop.maintained_visibility():
|
||||||
# Hide all other layers.
|
# Hide all other layers.
|
||||||
extract_ids = [
|
extract_ids = set([ll.id for ll in stub.
|
||||||
x.id for x in photoshop.get_layers_in_layers(layers)
|
get_layers_in_layers(layers)])
|
||||||
]
|
self.log.info("extract_ids {}".format(extract_ids))
|
||||||
for layer in photoshop.get_layers_in_document():
|
for layer in stub.get_layers():
|
||||||
if layer.id in extract_ids:
|
# limit unnecessary calls to client
|
||||||
layer.Visible = True
|
if layer.visible and layer.id not in extract_ids:
|
||||||
else:
|
stub.set_visible(layer.id, False)
|
||||||
layer.Visible = False
|
if not layer.visible and layer.id in extract_ids:
|
||||||
|
stub.set_visible(layer.id, True)
|
||||||
|
|
||||||
photoshop.app().ActiveDocument.SaveAs(
|
stub.saveAs(output_image_path, 'jpg', True)
|
||||||
output_image_path,
|
|
||||||
photoshop.com_objects.JPEGSaveOptions(),
|
|
||||||
True
|
|
||||||
)
|
|
||||||
|
|
||||||
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
|
ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg")
|
||||||
|
|
||||||
|
|
@ -66,8 +64,6 @@ class ExtractReview(pype.api.Extractor):
|
||||||
]
|
]
|
||||||
output = pype.lib._subprocess(args)
|
output = pype.lib._subprocess(args)
|
||||||
|
|
||||||
self.log.debug(output)
|
|
||||||
|
|
||||||
instance.data["representations"].append({
|
instance.data["representations"].append({
|
||||||
"name": "thumbnail",
|
"name": "thumbnail",
|
||||||
"ext": "jpg",
|
"ext": "jpg",
|
||||||
|
|
@ -75,7 +71,6 @@ class ExtractReview(pype.api.Extractor):
|
||||||
"stagingDir": staging_dir,
|
"stagingDir": staging_dir,
|
||||||
"tags": ["thumbnail"]
|
"tags": ["thumbnail"]
|
||||||
})
|
})
|
||||||
|
|
||||||
# Generate mov.
|
# Generate mov.
|
||||||
mov_path = os.path.join(staging_dir, "review.mov")
|
mov_path = os.path.join(staging_dir, "review.mov")
|
||||||
args = [
|
args = [
|
||||||
|
|
@ -86,9 +81,7 @@ class ExtractReview(pype.api.Extractor):
|
||||||
mov_path
|
mov_path
|
||||||
]
|
]
|
||||||
output = pype.lib._subprocess(args)
|
output = pype.lib._subprocess(args)
|
||||||
|
|
||||||
self.log.debug(output)
|
self.log.debug(output)
|
||||||
|
|
||||||
instance.data["representations"].append({
|
instance.data["representations"].append({
|
||||||
"name": "mov",
|
"name": "mov",
|
||||||
"ext": "mov",
|
"ext": "mov",
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,4 @@ class ExtractSaveScene(pype.api.Extractor):
|
||||||
families = ["workfile"]
|
families = ["workfile"]
|
||||||
|
|
||||||
def process(self, instance):
|
def process(self, instance):
|
||||||
photoshop.app().ActiveDocument.Save()
|
photoshop.stub().save()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import pyblish.api
|
import pyblish.api
|
||||||
from pype.action import get_errored_plugins_from_data
|
from pype.action import get_errored_plugins_from_data
|
||||||
from pype.lib import version_up
|
from pype.lib import version_up
|
||||||
|
|
||||||
from avalon import photoshop
|
from avalon import photoshop
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -24,6 +25,6 @@ class IncrementWorkfile(pyblish.api.InstancePlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
scene_path = version_up(instance.context.data["currentFile"])
|
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))
|
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
|
# Apply pyblish.logic to get the instances for the plug-in
|
||||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||||
|
stub = photoshop.stub()
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
data = photoshop.read(instance[0])
|
data = stub.read(instance[0])
|
||||||
|
|
||||||
data["asset"] = os.environ["AVALON_ASSET"]
|
data["asset"] = os.environ["AVALON_ASSET"]
|
||||||
photoshop.imprint(instance[0], data)
|
stub.imprint(instance[0], data)
|
||||||
|
|
||||||
|
|
||||||
class ValidateInstanceAsset(pyblish.api.InstancePlugin):
|
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
|
# Apply pyblish.logic to get the instances for the plug-in
|
||||||
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
instances = pyblish.api.instances_by_plugin(failed, plugin)
|
||||||
|
stub = photoshop.stub()
|
||||||
for instance in instances:
|
for instance in instances:
|
||||||
|
self.log.info("validate_naming instance {}".format(instance))
|
||||||
name = instance.data["name"].replace(" ", "_")
|
name = instance.data["name"].replace(" ", "_")
|
||||||
instance[0].Name = name
|
instance[0].Name = name
|
||||||
data = photoshop.read(instance[0])
|
data = stub.read(instance[0])
|
||||||
data["subset"] = "image" + name
|
data["subset"] = "image" + name
|
||||||
photoshop.imprint(instance[0], data)
|
stub.imprint(instance[0], data)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue