mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #2510 from pypeclub/feature/move_photoshop_to_openpype
Photoshop: Move implementation to OpenPype
This commit is contained in:
commit
7d13728371
43 changed files with 5521 additions and 157 deletions
255
openpype/hosts/photoshop/api/README.md
Normal file
255
openpype/hosts/photoshop/api/README.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
# Photoshop Integration
|
||||
|
||||
## Setup
|
||||
|
||||
The Photoshop integration requires two components to work; `extension` and `server`.
|
||||
|
||||
### Extension
|
||||
|
||||
To install the extension download [Extension Manager Command Line tool (ExManCmd)](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#option-2---exmancmd).
|
||||
|
||||
```
|
||||
ExManCmd /install {path to avalon-core}\avalon\photoshop\extension.zxp
|
||||
```
|
||||
|
||||
### Server
|
||||
|
||||
The easiest way to get the server and Photoshop launch is with:
|
||||
|
||||
```
|
||||
python -c ^"import avalon.photoshop;avalon.photoshop.launch(""C:\Program Files\Adobe\Adobe Photoshop 2020\Photoshop.exe"")^"
|
||||
```
|
||||
|
||||
`avalon.photoshop.launch` launches the application and server, and also closes the server when Photoshop exists.
|
||||
|
||||
## Usage
|
||||
|
||||
The Photoshop extension can be found under `Window > Extensions > Avalon`. Once launched you should be presented with a panel like this:
|
||||
|
||||

|
||||
|
||||
|
||||
## Developing
|
||||
|
||||
### Extension
|
||||
When developing the extension you can load it [unsigned](https://github.com/Adobe-CEP/CEP-Resources/blob/master/CEP_9.x/Documentation/CEP%209.0%20HTML%20Extension%20Cookbook.md#debugging-unsigned-extensions).
|
||||
|
||||
When signing the extension you can use this [guide](https://github.com/Adobe-CEP/Getting-Started-guides/tree/master/Package%20Distribute%20Install#package-distribute-install-guide).
|
||||
|
||||
```
|
||||
ZXPSignCmd -selfSignedCert NA NA Avalon Avalon-Photoshop avalon extension.p12
|
||||
ZXPSignCmd -sign {path to avalon-core}\avalon\photoshop\extension {path to avalon-core}\avalon\photoshop\extension.zxp extension.p12 avalon
|
||||
```
|
||||
|
||||
### Plugin Examples
|
||||
|
||||
These plugins were made with the [polly config](https://github.com/mindbender-studio/config). To fully integrate and load, you will have to use this config and add `image` to the [integration plugin](https://github.com/mindbender-studio/config/blob/master/polly/plugins/publish/integrate_asset.py).
|
||||
|
||||
#### Creator Plugin
|
||||
```python
|
||||
from avalon import photoshop
|
||||
|
||||
|
||||
class CreateImage(photoshop.Creator):
|
||||
"""Image folder for publish."""
|
||||
|
||||
name = "imageDefault"
|
||||
label = "Image"
|
||||
family = "image"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateImage, self).__init__(*args, **kwargs)
|
||||
```
|
||||
|
||||
#### Collector Plugin
|
||||
```python
|
||||
import pythoncom
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
"""Gather instances by LayerSet and file metadata
|
||||
|
||||
This collector takes into account assets that are associated with
|
||||
an LayerSet and marked with a unique identifier;
|
||||
|
||||
Identifier:
|
||||
id (str): "pyblish.avalon.instance"
|
||||
"""
|
||||
|
||||
label = "Instances"
|
||||
order = pyblish.api.CollectorOrder
|
||||
hosts = ["photoshop"]
|
||||
families_mapping = {
|
||||
"image": []
|
||||
}
|
||||
|
||||
def process(self, context):
|
||||
# Necessary call when running in a different thread which pyblish-qml
|
||||
# can be.
|
||||
pythoncom.CoInitialize()
|
||||
|
||||
photoshop_client = PhotoshopClientStub()
|
||||
layers = photoshop_client.get_layers()
|
||||
layers_meta = photoshop_client.get_layers_metadata()
|
||||
for layer in layers:
|
||||
layer_data = photoshop_client.read(layer, layers_meta)
|
||||
|
||||
# Skip layers without metadata.
|
||||
if layer_data is None:
|
||||
continue
|
||||
|
||||
# Skip containers.
|
||||
if "container" in layer_data["id"]:
|
||||
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.append(layer)
|
||||
instance.data.update(layer_data)
|
||||
instance.data["families"] = self.families_mapping[
|
||||
layer_data["family"]
|
||||
]
|
||||
instance.data["publish"] = layer.visible
|
||||
|
||||
# Produce diagnostic message for any graphical
|
||||
# user interface interested in visualising it.
|
||||
self.log.info("Found: \"%s\" " % instance.data["name"])
|
||||
```
|
||||
|
||||
#### Extractor Plugin
|
||||
```python
|
||||
import os
|
||||
|
||||
import openpype.api
|
||||
from avalon import photoshop
|
||||
|
||||
|
||||
class ExtractImage(openpype.api.Extractor):
|
||||
"""Produce a flattened image file from instance
|
||||
|
||||
This plug-in takes into account only the layers in the group.
|
||||
"""
|
||||
|
||||
label = "Extract Image"
|
||||
hosts = ["photoshop"]
|
||||
families = ["image"]
|
||||
formats = ["png", "jpg"]
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
staging_dir = self.staging_dir(instance)
|
||||
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 = set([ll.id for ll in stub.
|
||||
get_layers_in_layers([instance[0]])])
|
||||
|
||||
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)
|
||||
|
||||
save_options = []
|
||||
if "png" in self.formats:
|
||||
save_options.append('png')
|
||||
if "jpg" in self.formats:
|
||||
save_options.append('jpg')
|
||||
|
||||
file_basename = os.path.splitext(
|
||||
stub.get_active_document_name()
|
||||
)[0]
|
||||
for extension in save_options:
|
||||
_filename = "{}.{}".format(file_basename, extension)
|
||||
files[extension] = _filename
|
||||
|
||||
full_filename = os.path.join(staging_dir, _filename)
|
||||
stub.saveAs(full_filename, extension, True)
|
||||
|
||||
representations = []
|
||||
for extension, filename in files.items():
|
||||
representations.append({
|
||||
"name": extension,
|
||||
"ext": extension,
|
||||
"files": filename,
|
||||
"stagingDir": staging_dir
|
||||
})
|
||||
instance.data["representations"] = representations
|
||||
instance.data["stagingDir"] = staging_dir
|
||||
|
||||
self.log.info(f"Extracted {instance} to {staging_dir}")
|
||||
```
|
||||
|
||||
#### Loader Plugin
|
||||
```python
|
||||
from avalon import api, photoshop
|
||||
|
||||
stub = photoshop.stub()
|
||||
|
||||
|
||||
class ImageLoader(api.Loader):
|
||||
"""Load images
|
||||
|
||||
Stores the imported asset in a container named after the asset.
|
||||
"""
|
||||
|
||||
families = ["image"]
|
||||
representations = ["*"]
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
with photoshop.maintained_selection():
|
||||
layer = stub.import_smart_object(self.fname)
|
||||
|
||||
self[:] = [layer]
|
||||
|
||||
return photoshop.containerise(
|
||||
name,
|
||||
namespace,
|
||||
layer,
|
||||
context,
|
||||
self.__class__.__name__
|
||||
)
|
||||
|
||||
def update(self, container, representation):
|
||||
layer = container.pop("layer")
|
||||
|
||||
with photoshop.maintained_selection():
|
||||
stub.replace_smart_object(
|
||||
layer, api.get_representation_path(representation)
|
||||
)
|
||||
|
||||
stub.imprint(
|
||||
layer, {"representation": str(representation["_id"])}
|
||||
)
|
||||
|
||||
def remove(self, container):
|
||||
container["layer"].Delete()
|
||||
|
||||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
```
|
||||
For easier debugging of Javascript:
|
||||
https://community.adobe.com/t5/download-install/adobe-extension-debuger-problem/td-p/10911704?page=1
|
||||
Add --enable-blink-features=ShadowDOMV0,CustomElementsV0 when starting Chrome
|
||||
then localhost:8078 (port set in `photoshop\extension\.debug`)
|
||||
|
||||
Or use Visual Studio Code https://medium.com/adobetech/extendscript-debugger-for-visual-studio-code-public-release-a2ff6161fa01
|
||||
|
||||
Or install CEF client from https://github.com/Adobe-CEP/CEP-Resources/tree/master/CEP_9.x
|
||||
## Resources
|
||||
- https://github.com/lohriialo/photoshop-scripting-python
|
||||
- https://www.adobe.com/devnet/photoshop/scripting.html
|
||||
- https://github.com/Adobe-CEP/Getting-Started-guides
|
||||
- https://github.com/Adobe-CEP/CEP-Resources
|
||||
|
|
@ -1,79 +1,63 @@
|
|||
import os
|
||||
import sys
|
||||
import logging
|
||||
"""Public API
|
||||
|
||||
from Qt import QtWidgets
|
||||
Anything that isn't defined here is INTERNAL and unreliable for external use.
|
||||
|
||||
from avalon import io
|
||||
from avalon import api as avalon
|
||||
from openpype import lib
|
||||
from pyblish import api as pyblish
|
||||
import openpype.hosts.photoshop
|
||||
"""
|
||||
|
||||
log = logging.getLogger("openpype.hosts.photoshop")
|
||||
from .launch_logic import stub
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.photoshop.__file__))
|
||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
from .pipeline import (
|
||||
ls,
|
||||
list_instances,
|
||||
remove_instance,
|
||||
install,
|
||||
uninstall,
|
||||
containerise
|
||||
)
|
||||
from .plugin import (
|
||||
PhotoshopLoader,
|
||||
Creator,
|
||||
get_unique_layer_name
|
||||
)
|
||||
from .workio import (
|
||||
file_extensions,
|
||||
has_unsaved_changes,
|
||||
save_file,
|
||||
open_file,
|
||||
current_file,
|
||||
work_root,
|
||||
)
|
||||
|
||||
def check_inventory():
|
||||
if not lib.any_outdated():
|
||||
return
|
||||
from .lib import (
|
||||
maintained_selection,
|
||||
maintained_visibility
|
||||
)
|
||||
|
||||
host = avalon.registered_host()
|
||||
outdated_containers = []
|
||||
for container in host.ls():
|
||||
representation = container['representation']
|
||||
representation_doc = io.find_one(
|
||||
{
|
||||
"_id": io.ObjectId(representation),
|
||||
"type": "representation"
|
||||
},
|
||||
projection={"parent": True}
|
||||
)
|
||||
if representation_doc and not lib.is_latest(representation_doc):
|
||||
outdated_containers.append(container)
|
||||
__all__ = [
|
||||
# launch_logic
|
||||
"stub",
|
||||
|
||||
# Warn about outdated containers.
|
||||
print("Starting new QApplication..")
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
# pipeline
|
||||
"ls",
|
||||
"list_instances",
|
||||
"remove_instance",
|
||||
"install",
|
||||
"containerise",
|
||||
|
||||
message_box = QtWidgets.QMessageBox()
|
||||
message_box.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
msg = "There are outdated containers in the scene."
|
||||
message_box.setText(msg)
|
||||
message_box.exec_()
|
||||
# Plugin
|
||||
"PhotoshopLoader",
|
||||
"Creator",
|
||||
"get_unique_layer_name",
|
||||
|
||||
# Garbage collect QApplication.
|
||||
del app
|
||||
# workfiles
|
||||
"file_extensions",
|
||||
"has_unsaved_changes",
|
||||
"save_file",
|
||||
"open_file",
|
||||
"current_file",
|
||||
"work_root",
|
||||
|
||||
|
||||
def application_launch():
|
||||
check_inventory()
|
||||
|
||||
|
||||
def install():
|
||||
print("Installing Pype config...")
|
||||
|
||||
pyblish.register_plugin_path(PUBLISH_PATH)
|
||||
avalon.register_plugin_path(avalon.Loader, LOAD_PATH)
|
||||
avalon.register_plugin_path(avalon.Creator, CREATE_PATH)
|
||||
log.info(PUBLISH_PATH)
|
||||
|
||||
pyblish.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
avalon.on("application.launched", application_launch)
|
||||
|
||||
def uninstall():
|
||||
pyblish.deregister_plugin_path(PUBLISH_PATH)
|
||||
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
|
||||
avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
|
||||
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle layer visibility on instance toggles."""
|
||||
instance[0].Visible = new_value
|
||||
# lib
|
||||
"maintained_selection",
|
||||
"maintained_visibility",
|
||||
]
|
||||
|
|
|
|||
BIN
openpype/hosts/photoshop/api/extension.zxp
Normal file
BIN
openpype/hosts/photoshop/api/extension.zxp
Normal file
Binary file not shown.
9
openpype/hosts/photoshop/api/extension/.debug
Normal file
9
openpype/hosts/photoshop/api/extension/.debug
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ExtensionList>
|
||||
<Extension Id="com.openpype.PS.panel">
|
||||
<HostList>
|
||||
<Host Name="PHXS" Port="8078"/>
|
||||
<Host Name="FLPR" Port="8078"/>
|
||||
</HostList>
|
||||
</Extension>
|
||||
</ExtensionList>
|
||||
53
openpype/hosts/photoshop/api/extension/CSXS/manifest.xml
Normal file
53
openpype/hosts/photoshop/api/extension/CSXS/manifest.xml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<ExtensionManifest ExtensionBundleId="com.openpype.PS.panel" ExtensionBundleVersion="1.0.11" Version="7.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<ExtensionList>
|
||||
<Extension Id="com.openpype.PS.panel" Version="1.0.1" />
|
||||
</ExtensionList>
|
||||
<ExecutionEnvironment>
|
||||
<HostList>
|
||||
<Host Name="PHSP" Version="19" />
|
||||
<Host Name="PHXS" Version="19" />
|
||||
</HostList>
|
||||
<LocaleList>
|
||||
<Locale Code="All" />
|
||||
</LocaleList>
|
||||
<RequiredRuntimeList>
|
||||
<RequiredRuntime Name="CSXS" Version="7.0" />
|
||||
</RequiredRuntimeList>
|
||||
</ExecutionEnvironment>
|
||||
<DispatchInfoList>
|
||||
<Extension Id="com.openpype.PS.panel">
|
||||
<DispatchInfo>
|
||||
<Resources>
|
||||
<MainPath>./index.html</MainPath>
|
||||
<CEFCommandLine />
|
||||
</Resources>
|
||||
<Lifecycle>
|
||||
<AutoVisible>true</AutoVisible>
|
||||
<StartOn>
|
||||
<!-- Photoshop dispatches this event on startup -->
|
||||
<Event>applicationActivate</Event>
|
||||
<Event>com.adobe.csxs.events.ApplicationInitialized</Event>
|
||||
</StartOn>
|
||||
</Lifecycle>
|
||||
<UI>
|
||||
<Type>Panel</Type>
|
||||
<Menu>OpenPype</Menu>
|
||||
<Geometry>
|
||||
<Size>
|
||||
<Width>300</Width>
|
||||
<Height>140</Height>
|
||||
</Size>
|
||||
<MaxSize>
|
||||
<Width>400</Width>
|
||||
<Height>200</Height>
|
||||
</MaxSize>
|
||||
</Geometry>
|
||||
<Icons>
|
||||
<Icon Type="Normal">./icons/avalon-logo-48.png</Icon>
|
||||
</Icons>
|
||||
</UI>
|
||||
</DispatchInfo>
|
||||
</Extension>
|
||||
</DispatchInfoList>
|
||||
</ExtensionManifest>
|
||||
1193
openpype/hosts/photoshop/api/extension/client/CSInterface.js
Normal file
1193
openpype/hosts/photoshop/api/extension/client/CSInterface.js
Normal file
File diff suppressed because it is too large
Load diff
300
openpype/hosts/photoshop/api/extension/client/client.js
Normal file
300
openpype/hosts/photoshop/api/extension/client/client.js
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
// client facing part of extension, creates WSRPC client (jsx cannot
|
||||
// do that)
|
||||
// consumes RPC calls from server (OpenPype) calls ./host/index.jsx and
|
||||
// returns values back (in json format)
|
||||
|
||||
var logReturn = function(result){ log.warn('Result: ' + result);};
|
||||
|
||||
var csInterface = new CSInterface();
|
||||
|
||||
log.warn("script start");
|
||||
|
||||
WSRPC.DEBUG = false;
|
||||
WSRPC.TRACE = false;
|
||||
|
||||
function myCallBack(){
|
||||
log.warn("Triggered index.jsx");
|
||||
}
|
||||
// importing through manifest.xml isn't working because relative paths
|
||||
// possibly TODO
|
||||
jsx.evalFile('./host/index.jsx', myCallBack);
|
||||
|
||||
function runEvalScript(script) {
|
||||
// because of asynchronous nature of functions in jsx
|
||||
// this waits for response
|
||||
return new Promise(function(resolve, reject){
|
||||
csInterface.evalScript(script, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/** main entry point **/
|
||||
startUp("WEBSOCKET_URL");
|
||||
|
||||
// get websocket server url from environment value
|
||||
async function startUp(url){
|
||||
log.warn("url", url);
|
||||
promis = runEvalScript("getEnv('" + url + "')");
|
||||
|
||||
var res = await promis;
|
||||
// run rest only after resolved promise
|
||||
main(res);
|
||||
}
|
||||
|
||||
function get_extension_version(){
|
||||
/** Returns version number from extension manifest.xml **/
|
||||
log.debug("get_extension_version")
|
||||
var path = csInterface.getSystemPath(SystemPath.EXTENSION);
|
||||
log.debug("extension path " + path);
|
||||
|
||||
var result = window.cep.fs.readFile(path + "/CSXS/manifest.xml");
|
||||
var version = undefined;
|
||||
if(result.err === 0){
|
||||
if (window.DOMParser) {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(result.data.toString(), 'text/xml');
|
||||
const children = xmlDoc.children;
|
||||
|
||||
for (let i = 0; i <= children.length; i++) {
|
||||
if (children[i] && children[i].getAttribute('ExtensionBundleVersion')) {
|
||||
version = children[i].getAttribute('ExtensionBundleVersion');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
function main(websocket_url){
|
||||
// creates connection to 'websocket_url', registers routes
|
||||
log.warn("websocket_url", websocket_url);
|
||||
var default_url = 'ws://localhost:8099/ws/';
|
||||
|
||||
if (websocket_url == ''){
|
||||
websocket_url = default_url;
|
||||
}
|
||||
log.warn("connecting to:", websocket_url);
|
||||
RPC = new WSRPC(websocket_url, 5000); // spin connection
|
||||
|
||||
RPC.connect();
|
||||
|
||||
log.warn("connected");
|
||||
|
||||
function EscapeStringForJSX(str){
|
||||
// Replaces:
|
||||
// \ with \\
|
||||
// ' with \'
|
||||
// " with \"
|
||||
// See: https://stackoverflow.com/a/3967927/5285364
|
||||
return str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
RPC.addRoute('Photoshop.open', function (data) {
|
||||
log.warn('Server called client route "open":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.path);
|
||||
return runEvalScript("fileOpen('" + escapedPath +"')")
|
||||
.then(function(result){
|
||||
log.warn("open: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.read', function (data) {
|
||||
log.warn('Server called client route "read":', data);
|
||||
return runEvalScript("getHeadline()")
|
||||
.then(function(result){
|
||||
log.warn("getHeadline: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.get_layers', function (data) {
|
||||
log.warn('Server called client route "get_layers":', data);
|
||||
return runEvalScript("getLayers()")
|
||||
.then(function(result){
|
||||
log.warn("getLayers: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.set_visible', function (data) {
|
||||
log.warn('Server called client route "set_visible":', data);
|
||||
return runEvalScript("setVisible(" + data.layer_id + ", " +
|
||||
data.visibility + ")")
|
||||
.then(function(result){
|
||||
log.warn("setVisible: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.get_active_document_name', function (data) {
|
||||
log.warn('Server called client route "get_active_document_name":',
|
||||
data);
|
||||
return runEvalScript("getActiveDocumentName()")
|
||||
.then(function(result){
|
||||
log.warn("save: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.get_active_document_full_name', function (data) {
|
||||
log.warn('Server called client route ' +
|
||||
'"get_active_document_full_name":', data);
|
||||
return runEvalScript("getActiveDocumentFullName()")
|
||||
.then(function(result){
|
||||
log.warn("save: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.save', function (data) {
|
||||
log.warn('Server called client route "save":', data);
|
||||
|
||||
return runEvalScript("save()")
|
||||
.then(function(result){
|
||||
log.warn("save: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.get_selected_layers', function (data) {
|
||||
log.warn('Server called client route "get_selected_layers":', data);
|
||||
|
||||
return runEvalScript("getSelectedLayers()")
|
||||
.then(function(result){
|
||||
log.warn("get_selected_layers: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.create_group', function (data) {
|
||||
log.warn('Server called client route "create_group":', data);
|
||||
|
||||
return runEvalScript("createGroup('" + data.name + "')")
|
||||
.then(function(result){
|
||||
log.warn("createGroup: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.group_selected_layers', function (data) {
|
||||
log.warn('Server called client route "group_selected_layers":',
|
||||
data);
|
||||
|
||||
return runEvalScript("groupSelectedLayers(null, "+
|
||||
"'" + data.name +"')")
|
||||
.then(function(result){
|
||||
log.warn("group_selected_layers: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.import_smart_object', function (data) {
|
||||
log.warn('Server called client "import_smart_object":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.path);
|
||||
return runEvalScript("importSmartObject('" + escapedPath +"', " +
|
||||
"'"+ data.name +"',"+
|
||||
+ data.as_reference +")")
|
||||
.then(function(result){
|
||||
log.warn("import_smart_object: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.replace_smart_object', function (data) {
|
||||
log.warn('Server called route "replace_smart_object":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.path);
|
||||
return runEvalScript("replaceSmartObjects("+data.layer_id+"," +
|
||||
"'" + escapedPath +"',"+
|
||||
"'"+ data.name +"')")
|
||||
.then(function(result){
|
||||
log.warn("replaceSmartObjects: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.delete_layer', function (data) {
|
||||
log.warn('Server called route "delete_layer":', data);
|
||||
return runEvalScript("deleteLayer("+data.layer_id+")")
|
||||
.then(function(result){
|
||||
log.warn("delete_layer: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.rename_layer', function (data) {
|
||||
log.warn('Server called route "rename_layer":', data);
|
||||
return runEvalScript("renameLayer("+data.layer_id+", " +
|
||||
"'"+ data.name +"')")
|
||||
.then(function(result){
|
||||
log.warn("rename_layer: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.select_layers', function (data) {
|
||||
log.warn('Server called client route "select_layers":', data);
|
||||
|
||||
return runEvalScript("selectLayers('" + data.layers +"')")
|
||||
.then(function(result){
|
||||
log.warn("select_layers: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.is_saved', function (data) {
|
||||
log.warn('Server called client route "is_saved":', data);
|
||||
|
||||
return runEvalScript("isSaved()")
|
||||
.then(function(result){
|
||||
log.warn("is_saved: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.saveAs', function (data) {
|
||||
log.warn('Server called client route "saveAsJPEG":', data);
|
||||
var escapedPath = EscapeStringForJSX(data.image_path);
|
||||
return runEvalScript("saveAs('" + escapedPath + "', " +
|
||||
"'" + data.ext + "', " +
|
||||
data.as_copy + ")")
|
||||
.then(function(result){
|
||||
log.warn("save: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.imprint', function (data) {
|
||||
log.warn('Server called client route "imprint":', data);
|
||||
var escaped = data.payload.replace(/\n/g, "\\n");
|
||||
return runEvalScript("imprint('" + escaped + "')")
|
||||
.then(function(result){
|
||||
log.warn("imprint: " + result);
|
||||
return result;
|
||||
});
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.get_extension_version', function (data) {
|
||||
log.warn('Server called client route "get_extension_version":', data);
|
||||
return get_extension_version();
|
||||
});
|
||||
|
||||
RPC.addRoute('Photoshop.close', function (data) {
|
||||
log.warn('Server called client route "close":', data);
|
||||
return runEvalScript("close()");
|
||||
});
|
||||
|
||||
RPC.call('Photoshop.ping').then(function (data) {
|
||||
log.warn('Result for calling server route "ping": ', data);
|
||||
return runEvalScript("ping()")
|
||||
.then(function(result){
|
||||
log.warn("ping: " + result);
|
||||
return result;
|
||||
});
|
||||
|
||||
}, function (error) {
|
||||
log.warn(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
log.warn("end script");
|
||||
2
openpype/hosts/photoshop/api/extension/client/loglevel.min.js
vendored
Normal file
2
openpype/hosts/photoshop/api/extension/client/loglevel.min.js
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/*! loglevel - v1.6.8 - https://github.com/pimterry/loglevel - (c) 2020 Tim Perry - licensed MIT */
|
||||
!function(a,b){"use strict";"function"==typeof define&&define.amd?define(b):"object"==typeof module&&module.exports?module.exports=b():a.log=b()}(this,function(){"use strict";function a(a,b){var c=a[b];if("function"==typeof c.bind)return c.bind(a);try{return Function.prototype.bind.call(c,a)}catch(b){return function(){return Function.prototype.apply.apply(c,[a,arguments])}}}function b(){console.log&&(console.log.apply?console.log.apply(console,arguments):Function.prototype.apply.apply(console.log,[console,arguments])),console.trace&&console.trace()}function c(c){return"debug"===c&&(c="log"),typeof console!==i&&("trace"===c&&j?b:void 0!==console[c]?a(console,c):void 0!==console.log?a(console,"log"):h)}function d(a,b){for(var c=0;c<k.length;c++){var d=k[c];this[d]=c<a?h:this.methodFactory(d,a,b)}this.log=this.debug}function e(a,b,c){return function(){typeof console!==i&&(d.call(this,b,c),this[a].apply(this,arguments))}}function f(a,b,d){return c(a)||e.apply(this,arguments)}function g(a,b,c){function e(a){var b=(k[a]||"silent").toUpperCase();if(typeof window!==i){try{return void(window.localStorage[l]=b)}catch(a){}try{window.document.cookie=encodeURIComponent(l)+"="+b+";"}catch(a){}}}function g(){var a;if(typeof window!==i){try{a=window.localStorage[l]}catch(a){}if(typeof a===i)try{var b=window.document.cookie,c=b.indexOf(encodeURIComponent(l)+"=");-1!==c&&(a=/^([^;]+)/.exec(b.slice(c))[1])}catch(a){}return void 0===j.levels[a]&&(a=void 0),a}}var h,j=this,l="loglevel";a&&(l+=":"+a),j.name=a,j.levels={TRACE:0,DEBUG:1,INFO:2,WARN:3,ERROR:4,SILENT:5},j.methodFactory=c||f,j.getLevel=function(){return h},j.setLevel=function(b,c){if("string"==typeof b&&void 0!==j.levels[b.toUpperCase()]&&(b=j.levels[b.toUpperCase()]),!("number"==typeof b&&b>=0&&b<=j.levels.SILENT))throw"log.setLevel() called with invalid level: "+b;if(h=b,!1!==c&&e(b),d.call(j,b,a),typeof console===i&&b<j.levels.SILENT)return"No console available for logging"},j.setDefaultLevel=function(a){g()||j.setLevel(a,!1)},j.enableAll=function(a){j.setLevel(j.levels.TRACE,a)},j.disableAll=function(a){j.setLevel(j.levels.SILENT,a)};var m=g();null==m&&(m=null==b?"WARN":b),j.setLevel(m,!1)}var h=function(){},i="undefined",j=typeof window!==i&&typeof window.navigator!==i&&/Trident\/|MSIE /.test(window.navigator.userAgent),k=["trace","debug","info","warn","error"],l=new g,m={};l.getLogger=function(a){if("string"!=typeof a||""===a)throw new TypeError("You must supply a name when creating a logger.");var b=m[a];return b||(b=m[a]=new g(a,l.getLevel(),l.methodFactory)),b};var n=typeof window!==i?window.log:void 0;return l.noConflict=function(){return typeof window!==i&&window.log===l&&(window.log=n),l},l.getLoggers=function(){return m},l});
|
||||
393
openpype/hosts/photoshop/api/extension/client/wsrpc.js
Normal file
393
openpype/hosts/photoshop/api/extension/client/wsrpc.js
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = global || self, global.WSRPC = factory());
|
||||
}(this, function () { 'use strict';
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
var Deferred = function Deferred() {
|
||||
_classCallCheck(this, Deferred);
|
||||
|
||||
var self = this;
|
||||
self.resolve = null;
|
||||
self.reject = null;
|
||||
self.done = false;
|
||||
|
||||
function wrapper(func) {
|
||||
return function () {
|
||||
if (self.done) throw new Error('Promise already done');
|
||||
self.done = true;
|
||||
return func.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
self.promise = new Promise(function (resolve, reject) {
|
||||
self.resolve = wrapper(resolve);
|
||||
self.reject = wrapper(reject);
|
||||
});
|
||||
|
||||
self.promise.isPending = function () {
|
||||
return !self.done;
|
||||
};
|
||||
|
||||
return self;
|
||||
};
|
||||
|
||||
function logGroup(group, level, args) {
|
||||
console.group(group);
|
||||
console[level].apply(this, args);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
function log() {
|
||||
if (!WSRPC.DEBUG) return;
|
||||
logGroup('WSRPC.DEBUG', 'trace', arguments);
|
||||
}
|
||||
|
||||
function trace(msg) {
|
||||
if (!WSRPC.TRACE) return;
|
||||
var payload = msg;
|
||||
if ('data' in msg) payload = JSON.parse(msg.data);
|
||||
logGroup("WSRPC.TRACE", 'trace', [payload]);
|
||||
}
|
||||
|
||||
function getAbsoluteWsUrl(url) {
|
||||
if (/^\w+:\/\//.test(url)) return url;
|
||||
if (typeof window == 'undefined' && window.location.host.length < 1) throw new Error("Can not construct absolute URL from ".concat(window.location));
|
||||
var scheme = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
var port = window.location.port === '' ? ":".concat(window.location.port) : '';
|
||||
var host = window.location.host;
|
||||
var path = url.replace(/^\/+/gm, '');
|
||||
return "".concat(scheme, "//").concat(host).concat(port, "/").concat(path);
|
||||
}
|
||||
|
||||
var readyState = Object.freeze({
|
||||
0: 'CONNECTING',
|
||||
1: 'OPEN',
|
||||
2: 'CLOSING',
|
||||
3: 'CLOSED'
|
||||
});
|
||||
|
||||
var WSRPC = function WSRPC(URL) {
|
||||
var reconnectTimeout = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1000;
|
||||
|
||||
_classCallCheck(this, WSRPC);
|
||||
|
||||
var self = this;
|
||||
URL = getAbsoluteWsUrl(URL);
|
||||
self.id = 1;
|
||||
self.eventId = 0;
|
||||
self.socketStarted = false;
|
||||
self.eventStore = {
|
||||
onconnect: {},
|
||||
onerror: {},
|
||||
onclose: {},
|
||||
onchange: {}
|
||||
};
|
||||
self.connectionNumber = 0;
|
||||
self.oneTimeEventStore = {
|
||||
onconnect: [],
|
||||
onerror: [],
|
||||
onclose: [],
|
||||
onchange: []
|
||||
};
|
||||
self.callQueue = [];
|
||||
|
||||
function createSocket() {
|
||||
var ws = new WebSocket(URL);
|
||||
|
||||
var rejectQueue = function rejectQueue() {
|
||||
self.connectionNumber++; // rejects incoming calls
|
||||
|
||||
var deferred; //reject all pending calls
|
||||
|
||||
while (0 < self.callQueue.length) {
|
||||
var callObj = self.callQueue.shift();
|
||||
deferred = self.store[callObj.id];
|
||||
delete self.store[callObj.id];
|
||||
|
||||
if (deferred && deferred.promise.isPending()) {
|
||||
deferred.reject('WebSocket error occurred');
|
||||
}
|
||||
} // reject all from the store
|
||||
|
||||
|
||||
for (var key in self.store) {
|
||||
if (!self.store.hasOwnProperty(key)) continue;
|
||||
deferred = self.store[key];
|
||||
|
||||
if (deferred && deferred.promise.isPending()) {
|
||||
deferred.reject('WebSocket error occurred');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function reconnect(callEvents) {
|
||||
setTimeout(function () {
|
||||
try {
|
||||
self.socket = createSocket();
|
||||
self.id = 1;
|
||||
} catch (exc) {
|
||||
callEvents('onerror', exc);
|
||||
delete self.socket;
|
||||
console.error(exc);
|
||||
}
|
||||
}, reconnectTimeout);
|
||||
}
|
||||
|
||||
ws.onclose = function (err) {
|
||||
log('ONCLOSE CALLED', 'STATE', self.public.state());
|
||||
trace(err);
|
||||
|
||||
for (var serial in self.store) {
|
||||
if (!self.store.hasOwnProperty(serial)) continue;
|
||||
|
||||
if (self.store[serial].hasOwnProperty('reject')) {
|
||||
self.store[serial].reject('Connection closed');
|
||||
}
|
||||
}
|
||||
|
||||
rejectQueue();
|
||||
callEvents('onclose', err);
|
||||
callEvents('onchange', err);
|
||||
reconnect(callEvents);
|
||||
};
|
||||
|
||||
ws.onerror = function (err) {
|
||||
log('ONERROR CALLED', 'STATE', self.public.state());
|
||||
trace(err);
|
||||
rejectQueue();
|
||||
callEvents('onerror', err);
|
||||
callEvents('onchange', err);
|
||||
log('WebSocket has been closed by error: ', err);
|
||||
};
|
||||
|
||||
function tryCallEvent(func, event) {
|
||||
try {
|
||||
return func(event);
|
||||
} catch (e) {
|
||||
if (e.hasOwnProperty('stack')) {
|
||||
log(e.stack);
|
||||
} else {
|
||||
log('Event function', func, 'raised unknown error:', e);
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function callEvents(evName, event) {
|
||||
while (0 < self.oneTimeEventStore[evName].length) {
|
||||
var deferred = self.oneTimeEventStore[evName].shift();
|
||||
if (deferred.hasOwnProperty('resolve') && deferred.promise.isPending()) deferred.resolve();
|
||||
}
|
||||
|
||||
for (var i in self.eventStore[evName]) {
|
||||
if (!self.eventStore[evName].hasOwnProperty(i)) continue;
|
||||
var cur = self.eventStore[evName][i];
|
||||
tryCallEvent(cur, event);
|
||||
}
|
||||
}
|
||||
|
||||
ws.onopen = function (ev) {
|
||||
log('ONOPEN CALLED', 'STATE', self.public.state());
|
||||
trace(ev);
|
||||
|
||||
while (0 < self.callQueue.length) {
|
||||
// noinspection JSUnresolvedFunction
|
||||
self.socket.send(JSON.stringify(self.callQueue.shift(), 0, 1));
|
||||
}
|
||||
|
||||
callEvents('onconnect', ev);
|
||||
callEvents('onchange', ev);
|
||||
};
|
||||
|
||||
function handleCall(self, data) {
|
||||
if (!self.routes.hasOwnProperty(data.method)) throw new Error('Route not found');
|
||||
var connectionNumber = self.connectionNumber;
|
||||
var deferred = new Deferred();
|
||||
deferred.promise.then(function (result) {
|
||||
if (connectionNumber !== self.connectionNumber) return;
|
||||
self.socket.send(JSON.stringify({
|
||||
id: data.id,
|
||||
result: result
|
||||
}));
|
||||
}, function (error) {
|
||||
if (connectionNumber !== self.connectionNumber) return;
|
||||
self.socket.send(JSON.stringify({
|
||||
id: data.id,
|
||||
error: error
|
||||
}));
|
||||
});
|
||||
var func = self.routes[data.method];
|
||||
if (self.asyncRoutes[data.method]) return func.apply(deferred, [data.params]);
|
||||
|
||||
function badPromise() {
|
||||
throw new Error("You should register route with async flag.");
|
||||
}
|
||||
|
||||
var promiseMock = {
|
||||
resolve: badPromise,
|
||||
reject: badPromise
|
||||
};
|
||||
|
||||
try {
|
||||
deferred.resolve(func.apply(promiseMock, [data.params]));
|
||||
} catch (e) {
|
||||
deferred.reject(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError(self, data) {
|
||||
if (!self.store.hasOwnProperty(data.id)) return log('Unknown callback');
|
||||
var deferred = self.store[data.id];
|
||||
if (typeof deferred === 'undefined') return log('Confirmation without handler');
|
||||
delete self.store[data.id];
|
||||
log('REJECTING', data.error);
|
||||
deferred.reject(data.error);
|
||||
}
|
||||
|
||||
function handleResult(self, data) {
|
||||
var deferred = self.store[data.id];
|
||||
if (typeof deferred === 'undefined') return log('Confirmation without handler');
|
||||
delete self.store[data.id];
|
||||
|
||||
if (data.hasOwnProperty('result')) {
|
||||
return deferred.resolve(data.result);
|
||||
}
|
||||
|
||||
return deferred.reject(data.error);
|
||||
}
|
||||
|
||||
ws.onmessage = function (message) {
|
||||
log('ONMESSAGE CALLED', 'STATE', self.public.state());
|
||||
trace(message);
|
||||
if (message.type !== 'message') return;
|
||||
var data;
|
||||
|
||||
try {
|
||||
data = JSON.parse(message.data);
|
||||
log(data);
|
||||
|
||||
if (data.hasOwnProperty('method')) {
|
||||
return handleCall(self, data);
|
||||
} else if (data.hasOwnProperty('error') && data.error === null) {
|
||||
return handleError(self, data);
|
||||
} else {
|
||||
return handleResult(self, data);
|
||||
}
|
||||
} catch (exception) {
|
||||
var err = {
|
||||
error: exception.message,
|
||||
result: null,
|
||||
id: data ? data.id : null
|
||||
};
|
||||
self.socket.send(JSON.stringify(err));
|
||||
console.error(exception);
|
||||
}
|
||||
};
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
function makeCall(func, args, params) {
|
||||
self.id += 2;
|
||||
var deferred = new Deferred();
|
||||
var callObj = Object.freeze({
|
||||
id: self.id,
|
||||
method: func,
|
||||
params: args
|
||||
});
|
||||
var state = self.public.state();
|
||||
|
||||
if (state === 'OPEN') {
|
||||
self.store[self.id] = deferred;
|
||||
self.socket.send(JSON.stringify(callObj));
|
||||
} else if (state === 'CONNECTING') {
|
||||
log('SOCKET IS', state);
|
||||
self.store[self.id] = deferred;
|
||||
self.callQueue.push(callObj);
|
||||
} else {
|
||||
log('SOCKET IS', state);
|
||||
|
||||
if (params && params['noWait']) {
|
||||
deferred.reject("Socket is: ".concat(state));
|
||||
} else {
|
||||
self.store[self.id] = deferred;
|
||||
self.callQueue.push(callObj);
|
||||
}
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
self.asyncRoutes = {};
|
||||
self.routes = {};
|
||||
self.store = {};
|
||||
self.public = Object.freeze({
|
||||
call: function call(func, args, params) {
|
||||
return makeCall(func, args, params);
|
||||
},
|
||||
addRoute: function addRoute(route, callback, isAsync) {
|
||||
self.asyncRoutes[route] = isAsync || false;
|
||||
self.routes[route] = callback;
|
||||
},
|
||||
deleteRoute: function deleteRoute(route) {
|
||||
delete self.asyncRoutes[route];
|
||||
return delete self.routes[route];
|
||||
},
|
||||
addEventListener: function addEventListener(event, func) {
|
||||
var eventId = self.eventId++;
|
||||
self.eventStore[event][eventId] = func;
|
||||
return eventId;
|
||||
},
|
||||
removeEventListener: function removeEventListener(event, index) {
|
||||
if (self.eventStore[event].hasOwnProperty(index)) {
|
||||
delete self.eventStore[event][index];
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onEvent: function onEvent(event) {
|
||||
var deferred = new Deferred();
|
||||
self.oneTimeEventStore[event].push(deferred);
|
||||
return deferred.promise;
|
||||
},
|
||||
destroy: function destroy() {
|
||||
return self.socket.close();
|
||||
},
|
||||
state: function state() {
|
||||
return readyState[this.stateCode()];
|
||||
},
|
||||
stateCode: function stateCode() {
|
||||
if (self.socketStarted && self.socket) return self.socket.readyState;
|
||||
return 3;
|
||||
},
|
||||
connect: function connect() {
|
||||
self.socketStarted = true;
|
||||
self.socket = createSocket();
|
||||
}
|
||||
});
|
||||
self.public.addRoute('log', function (argsObj) {
|
||||
//console.info("Websocket sent: ".concat(argsObj));
|
||||
});
|
||||
self.public.addRoute('ping', function (data) {
|
||||
return data;
|
||||
});
|
||||
return self.public;
|
||||
};
|
||||
|
||||
WSRPC.DEBUG = false;
|
||||
WSRPC.TRACE = false;
|
||||
|
||||
return WSRPC;
|
||||
|
||||
}));
|
||||
//# sourceMappingURL=wsrpc.js.map
|
||||
1
openpype/hosts/photoshop/api/extension/client/wsrpc.min.js
vendored
Normal file
1
openpype/hosts/photoshop/api/extension/client/wsrpc.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
774
openpype/hosts/photoshop/api/extension/host/JSX.js
Normal file
774
openpype/hosts/photoshop/api/extension/host/JSX.js
Normal file
|
|
@ -0,0 +1,774 @@
|
|||
/*
|
||||
_ ______ __ _
|
||||
| / ___\ \/ / (_)___
|
||||
_ | \___ \\ / | / __|
|
||||
| |_| |___) / \ _ | \__ \
|
||||
\___/|____/_/\_(_)/ |___/
|
||||
|__/
|
||||
_ ____
|
||||
/\ /\___ _ __ ___(_) ___ _ __ |___ \
|
||||
\ \ / / _ \ '__/ __| |/ _ \| '_ \ __) |
|
||||
\ V / __/ | \__ \ | (_) | | | | / __/
|
||||
\_/ \___|_| |___/_|\___/|_| |_| |_____|
|
||||
*/
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// JSX.js © and writtent by Trevor https://creative-scripts.com/jsx-js //
|
||||
// If you turn over is less the $50,000,000 then you don't have to pay anything //
|
||||
// License MIT, don't complain, don't sue NO MATTER WHAT //
|
||||
// If you turn over is more the $50,000,000 then you DO have to pay //
|
||||
// Contact me https://creative-scripts.com/contact for pricing and licensing //
|
||||
// Don't remove these commented lines //
|
||||
// For simple and effective calling of jsx from the js engine //
|
||||
// Version 2 last modified April 18 2018 //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Change log: //
|
||||
// JSX.js V2 is now independent of NodeJS and CSInterface.js <span class="wp-font-emots-emo-happy"></span> //
|
||||
// forceEval is now by default true //
|
||||
// It wraps the scripts in a try catch and an eval providing useful error handling //
|
||||
// One can set in the jsx engine $.includeStack = true to return the call stack in the event of an error //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// JSX.js for calling jsx code from the js engine //
|
||||
// 2 methods included //
|
||||
// 1) jsx.evalScript AKA jsx.eval //
|
||||
// 2) jsx.evalFile AKA jsx.file //
|
||||
// Special features //
|
||||
// 1) Allows all changes in your jsx code to be reloaded into your extension at the click of a button //
|
||||
// 2) Can enable the $.fileName property to work and provides a $.__fileName() method as an alternative //
|
||||
// 3) Can force a callBack result from InDesign //
|
||||
// 4) No more csInterface.evalScript('alert("hello "' + title + " " + name + '");') //
|
||||
// use jsx.evalScript('alert("hello __title__ __name__");', {title: title, name: name}); //
|
||||
// 5) execute jsx files from your jsx folder like this jsx.evalFile('myFabJsxScript.jsx'); //
|
||||
// or from a relative path jsx.evalFile('../myFabScripts/myFabJsxScript.jsx'); //
|
||||
// or from an absolute url jsx.evalFile('/Path/to/my/FabJsxScript.jsx'); (mac) //
|
||||
// or from an absolute url jsx.evalFile('C:Path/to/my/FabJsxScript.jsx'); (windows) //
|
||||
// 6) Parameter can be entered in the from of a parameter list which can be in any order or as an object //
|
||||
// 7) Not camelCase sensitive (very useful for the illiterate) //
|
||||
// <span class="wp-font-emots-emo-sunglasses"></span> Dead easy to use BUT SPEND THE 3 TO 5 MINUTES IT SHOULD TAKE TO READ THE INSTRUCTIONS //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/* jshint undef:true, unused:true, esversion:6 */
|
||||
|
||||
//////////////////////////////////////
|
||||
// jsx is the interface for the API //
|
||||
//////////////////////////////////////
|
||||
|
||||
var jsx;
|
||||
|
||||
// Wrap everything in an anonymous function to prevent leeks
|
||||
(function() {
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// Substitute some CSInterface functions to avoid dependency on it //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
var __dirname = (function() {
|
||||
var path, isMac;
|
||||
path = decodeURI(window.__adobe_cep__.getSystemPath('extension'));
|
||||
isMac = navigator.platform[0] === 'M'; // [M]ac
|
||||
path = path.replace('file://' + (isMac ? '' : '/'), '');
|
||||
return path;
|
||||
})();
|
||||
|
||||
var evalScript = function(script, callback) {
|
||||
callback = callback || function() {};
|
||||
window.__adobe_cep__.evalScript(script, callback);
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////
|
||||
// In place of using the node path module //
|
||||
////////////////////////////////////////////
|
||||
|
||||
// jshint undef: true, unused: true
|
||||
|
||||
// A very minified version of the NodeJs Path module!!
|
||||
// For use outside of NodeJs
|
||||
// Majorly nicked by Trevor from Joyent
|
||||
var path = (function() {
|
||||
|
||||
var isString = function(arg) {
|
||||
return typeof arg === 'string';
|
||||
};
|
||||
|
||||
// var isObject = function(arg) {
|
||||
// return typeof arg === 'object' && arg !== null;
|
||||
// };
|
||||
|
||||
var basename = function(path) {
|
||||
if (!isString(path)) {
|
||||
throw new TypeError('Argument to path.basename must be a string');
|
||||
}
|
||||
var bits = path.split(/[\/\\]/g);
|
||||
return bits[bits.length - 1];
|
||||
};
|
||||
|
||||
// jshint undef: true
|
||||
// Regex to split a windows path into three parts: [*, device, slash,
|
||||
// tail] windows-only
|
||||
var splitDeviceRe =
|
||||
/^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/;
|
||||
|
||||
// Regex to split the tail part of the above into [*, dir, basename, ext]
|
||||
// var splitTailRe =
|
||||
// /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/;
|
||||
|
||||
var win32 = {};
|
||||
// Function to split a filename into [root, dir, basename, ext]
|
||||
// var win32SplitPath = function(filename) {
|
||||
// // Separate device+slash from tail
|
||||
// var result = splitDeviceRe.exec(filename),
|
||||
// device = (result[1] || '') + (result[2] || ''),
|
||||
// tail = result[3] || '';
|
||||
// // Split the tail into dir, basename and extension
|
||||
// var result2 = splitTailRe.exec(tail),
|
||||
// dir = result2[1],
|
||||
// basename = result2[2],
|
||||
// ext = result2[3];
|
||||
// return [device, dir, basename, ext];
|
||||
// };
|
||||
|
||||
var win32StatPath = function(path) {
|
||||
var result = splitDeviceRe.exec(path),
|
||||
device = result[1] || '',
|
||||
isUnc = !!device && device[1] !== ':';
|
||||
return {
|
||||
device: device,
|
||||
isUnc: isUnc,
|
||||
isAbsolute: isUnc || !!result[2], // UNC paths are always absolute
|
||||
tail: result[3]
|
||||
};
|
||||
};
|
||||
|
||||
var normalizeUNCRoot = function(device) {
|
||||
return '\\\\' + device.replace(/^[\\\/]+/, '').replace(/[\\\/]+/g, '\\');
|
||||
};
|
||||
|
||||
var normalizeArray = function(parts, allowAboveRoot) {
|
||||
var res = [];
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i];
|
||||
|
||||
// ignore empty parts
|
||||
if (!p || p === '.')
|
||||
continue;
|
||||
|
||||
if (p === '..') {
|
||||
if (res.length && res[res.length - 1] !== '..') {
|
||||
res.pop();
|
||||
} else if (allowAboveRoot) {
|
||||
res.push('..');
|
||||
}
|
||||
} else {
|
||||
res.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
win32.normalize = function(path) {
|
||||
var result = win32StatPath(path),
|
||||
device = result.device,
|
||||
isUnc = result.isUnc,
|
||||
isAbsolute = result.isAbsolute,
|
||||
tail = result.tail,
|
||||
trailingSlash = /[\\\/]$/.test(tail);
|
||||
|
||||
// Normalize the tail path
|
||||
tail = normalizeArray(tail.split(/[\\\/]+/), !isAbsolute).join('\\');
|
||||
|
||||
if (!tail && !isAbsolute) {
|
||||
tail = '.';
|
||||
}
|
||||
if (tail && trailingSlash) {
|
||||
tail += '\\';
|
||||
}
|
||||
|
||||
// Convert slashes to backslashes when `device` points to an UNC root.
|
||||
// Also squash multiple slashes into a single one where appropriate.
|
||||
if (isUnc) {
|
||||
device = normalizeUNCRoot(device);
|
||||
}
|
||||
|
||||
return device + (isAbsolute ? '\\' : '') + tail;
|
||||
};
|
||||
win32.join = function() {
|
||||
var paths = [];
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var arg = arguments[i];
|
||||
if (!isString(arg)) {
|
||||
throw new TypeError('Arguments to path.join must be strings');
|
||||
}
|
||||
if (arg) {
|
||||
paths.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
var joined = paths.join('\\');
|
||||
|
||||
// Make sure that the joined path doesn't start with two slashes, because
|
||||
// normalize() will mistake it for an UNC path then.
|
||||
//
|
||||
// This step is skipped when it is very clear that the user actually
|
||||
// intended to point at an UNC path. This is assumed when the first
|
||||
// non-empty string arguments starts with exactly two slashes followed by
|
||||
// at least one more non-slash character.
|
||||
//
|
||||
// Note that for normalize() to treat a path as an UNC path it needs to
|
||||
// have at least 2 components, so we don't filter for that here.
|
||||
// This means that the user can use join to construct UNC paths from
|
||||
// a server name and a share name; for example:
|
||||
// path.join('//server', 'share') -> '\\\\server\\share\')
|
||||
if (!/^[\\\/]{2}[^\\\/]/.test(paths[0])) {
|
||||
joined = joined.replace(/^[\\\/]{2,}/, '\\');
|
||||
}
|
||||
return win32.normalize(joined);
|
||||
};
|
||||
|
||||
var posix = {};
|
||||
|
||||
// posix version
|
||||
posix.join = function() {
|
||||
var path = '';
|
||||
for (var i = 0; i < arguments.length; i++) {
|
||||
var segment = arguments[i];
|
||||
if (!isString(segment)) {
|
||||
throw new TypeError('Arguments to path.join must be strings');
|
||||
}
|
||||
if (segment) {
|
||||
if (!path) {
|
||||
path += segment;
|
||||
} else {
|
||||
path += '/' + segment;
|
||||
}
|
||||
}
|
||||
}
|
||||
return posix.normalize(path);
|
||||
};
|
||||
|
||||
// path.normalize(path)
|
||||
// posix version
|
||||
posix.normalize = function(path) {
|
||||
var isAbsolute = path.charAt(0) === '/',
|
||||
trailingSlash = path && path[path.length - 1] === '/';
|
||||
|
||||
// Normalize the path
|
||||
path = normalizeArray(path.split('/'), !isAbsolute).join('/');
|
||||
|
||||
if (!path && !isAbsolute) {
|
||||
path = '.';
|
||||
}
|
||||
if (path && trailingSlash) {
|
||||
path += '/';
|
||||
}
|
||||
|
||||
return (isAbsolute ? '/' : '') + path;
|
||||
};
|
||||
|
||||
win32.basename = posix.basename = basename;
|
||||
|
||||
this.win32 = win32;
|
||||
this.posix = posix;
|
||||
return (navigator.platform[0] === 'M') ? posix : win32;
|
||||
})();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// The is the "main" function which is to be prototyped //
|
||||
// It run a small snippet in the jsx engine that //
|
||||
// 1) Assigns $.__dirname with the value of the extensions __dirname base path //
|
||||
// 2) Sets up a method $.__fileName() for retrieving from within the jsx script it's $.fileName value //
|
||||
// more on that method later //
|
||||
// At the end of the script the global declaration jsx = new Jsx(); has been made. //
|
||||
// If you like you can remove that and include in your relevant functions //
|
||||
// var jsx = new Jsx(); You would never call the Jsx function without the "new" declaration //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
var Jsx = function() {
|
||||
var jsxScript;
|
||||
// Setup jsx function to enable the jsx scripts to easily retrieve their file location
|
||||
jsxScript = [
|
||||
'$.level = 0;',
|
||||
'if(!$.__fileNames){',
|
||||
' $.__fileNames = {};',
|
||||
' $.__dirname = "__dirname__";'.replace('__dirname__', __dirname),
|
||||
' $.__fileName = function(name){',
|
||||
' name = name || $.fileName;',
|
||||
' return ($.__fileNames && $.__fileNames[name]) || $.fileName;',
|
||||
' };',
|
||||
'}'
|
||||
].join('');
|
||||
evalScript(jsxScript);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* [evalScript] For calling jsx scripts from the js engine
|
||||
*
|
||||
* The jsx.evalScript method is used for calling jsx scripts directly from the js engine
|
||||
* Allows for easy replacement i.e. variable insertions and for forcing eval.
|
||||
* For convenience jsx.eval or jsx.script or jsx.evalscript can be used instead of calling jsx.evalScript
|
||||
*
|
||||
* @param {String} jsxScript
|
||||
* The string that makes up the jsx script
|
||||
* it can contain a simple template like syntax for replacements
|
||||
* 'alert("__foo__");'
|
||||
* the __foo__ will be replaced as per the replacements parameter
|
||||
*
|
||||
* @param {Function} callback
|
||||
* The callback function you want the jsx script to trigger on completion
|
||||
* The result of the jsx script is passed as the argument to that function
|
||||
* The function can exist in some other file.
|
||||
* Note that InDesign does not automatically pass the callBack as a string.
|
||||
* Either write your InDesign in a way that it returns a sting the form of
|
||||
* return 'this is my result surrounded by quotes'
|
||||
* or use the force eval option
|
||||
* [Optional DEFAULT no callBack]
|
||||
*
|
||||
* @param {Object} replacements
|
||||
* The replacements to make on the jsx script
|
||||
* given the following script (template)
|
||||
* 'alert("__message__: " + __val__);'
|
||||
* and we want to change the script to
|
||||
* 'alert("I was born in the year: " + 1234);'
|
||||
* we would pass the following object
|
||||
* {"message": 'I was born in the year', "val": 1234}
|
||||
* or if not using reserved words like do we can leave out the key quotes
|
||||
* {message: 'I was born in the year', val: 1234}
|
||||
* [Optional DEFAULT no replacements]
|
||||
*
|
||||
* @param {Bolean} forceEval
|
||||
* If the script should be wrapped in an eval and try catch
|
||||
* This will 1) provide useful error feedback if heaven forbid it is needed
|
||||
* 2) The result will be a string which is required for callback results in InDesign
|
||||
* [Optional DEFAULT true]
|
||||
*
|
||||
* Note 1) The order of the parameters is irrelevant
|
||||
* Note 2) One can pass the arguments as an object if desired
|
||||
* jsx.evalScript(myCallBackFunction, 'alert("__myMessage__");', true);
|
||||
* is the same as
|
||||
* jsx.evalScript({
|
||||
* script: 'alert("__myMessage__");',
|
||||
* replacements: {myMessage: 'Hi there'},
|
||||
* callBack: myCallBackFunction,
|
||||
* eval: true
|
||||
* });
|
||||
* note that either lower or camelCase key names are valid
|
||||
* i.e. both callback or callBack will work
|
||||
*
|
||||
* The following keys are the same jsx || script || jsxScript || jsxscript || file
|
||||
* The following keys are the same callBack || callback
|
||||
* The following keys are the same replacements || replace
|
||||
* The following keys are the same eval || forceEval || forceeval
|
||||
* The following keys are the same forceEvalScript || forceevalscript || evalScript || evalscript;
|
||||
*
|
||||
* @return {Boolean} if the jsxScript was executed or not
|
||||
*/
|
||||
|
||||
Jsx.prototype.evalScript = function() {
|
||||
var arg, i, key, replaceThis, withThis, args, callback, forceEval, replacements, jsxScript, isBin;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// sort out order which arguments into jsxScript, callback, replacements, forceEval //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
args = arguments;
|
||||
|
||||
// Detect if the parameters were passed as an object and if so allow for various keys
|
||||
if (args.length === 1 && (arg = args[0]) instanceof Object) {
|
||||
jsxScript = arg.jsxScript || arg.jsx || arg.script || arg.file || arg.jsxscript;
|
||||
callback = arg.callBack || arg.callback;
|
||||
replacements = arg.replacements || arg.replace;
|
||||
forceEval = arg.eval || arg.forceEval || arg.forceeval;
|
||||
} else {
|
||||
for (i = 0; i < 4; i++) {
|
||||
arg = args[i];
|
||||
if (arg === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (arg.constructor === String) {
|
||||
jsxScript = arg;
|
||||
continue;
|
||||
}
|
||||
if (arg.constructor === Object) {
|
||||
replacements = arg;
|
||||
continue;
|
||||
}
|
||||
if (arg.constructor === Function) {
|
||||
callback = arg;
|
||||
continue;
|
||||
}
|
||||
if (arg === false) {
|
||||
forceEval = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no script provide then not too much to do!
|
||||
if (!jsxScript) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Have changed the forceEval default to be true as I prefer the error handling
|
||||
if (forceEval !== false) {
|
||||
forceEval = true;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// On Illustrator and other apps the result of the jsx script is automatically passed as a string //
|
||||
// if you have a "script" containing the single number 1 and nothing else then the callBack will register as "1" //
|
||||
// On InDesign that same script will provide a blank callBack //
|
||||
// Let's say we have a callBack function var callBack = function(result){alert(result);} //
|
||||
// On Ai your see the 1 in the alert //
|
||||
// On ID your just see a blank alert //
|
||||
// To see the 1 in the alert you need to convert the result to a string and then it will show //
|
||||
// So if we rewrite out 1 byte script to '1' i.e. surround the 1 in quotes then the call back alert will show 1 //
|
||||
// If the scripts planed one can make sure that the results always passed as a string (including errors) //
|
||||
// otherwise one can wrap the script in an eval and then have the result passed as a string //
|
||||
// I have not gone through all the apps but can say //
|
||||
// for Ai you never need to set the forceEval to true //
|
||||
// for ID you if you have not coded your script appropriately and your want to send a result to the callBack then set forceEval to true //
|
||||
// I changed this that even on Illustrator it applies the try catch, Note the try catch will fail if $.level is set to 1 //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
if (forceEval) {
|
||||
|
||||
isBin = (jsxScript.substring(0, 10) === '@JSXBIN@ES') ? '' : '\n';
|
||||
jsxScript = (
|
||||
// "\n''') + '';} catch(e){(function(e){var n, a=[]; for (n in e){a.push(n + ': ' + e[n])}; return a.join('\n')})(e)}");
|
||||
// "\n''') + '';} catch(e){e + (e.line ? ('\\nLine ' + (+e.line - 1)) : '')}");
|
||||
[
|
||||
"$.level = 0;",
|
||||
"try{eval('''" + isBin, // need to add an extra line otherwise #targetengine doesn't work ;-]
|
||||
jsxScript.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"') + "\n''') + '';",
|
||||
"} catch (e) {",
|
||||
" (function(e) {",
|
||||
" var line, sourceLine, name, description, ErrorMessage, fileName, start, end, bug;",
|
||||
" line = +e.line" + (isBin === '' ? ';' : ' - 1;'), // To take into account the extra line added
|
||||
" fileName = File(e.fileName).fsName;",
|
||||
" sourceLine = line && e.source.split(/[\\r\\n]/)[line];",
|
||||
" name = e.name;",
|
||||
" description = e.description;",
|
||||
" ErrorMessage = name + ' ' + e.number + ': ' + description;",
|
||||
" if (fileName.length && !(/[\\/\\\\]\\d+$/.test(fileName))) {",
|
||||
" ErrorMessage += '\\nFile: ' + fileName;",
|
||||
" line++;",
|
||||
" }",
|
||||
" if (line){",
|
||||
" ErrorMessage += '\\nLine: ' + line +",
|
||||
" '-> ' + ((sourceLine.length < 300) ? sourceLine : sourceLine.substring(0,300) + '...');",
|
||||
" }",
|
||||
" if (e.start) {ErrorMessage += '\\nBug: ' + e.source.substring(e.start - 1, e.end)}",
|
||||
" if ($.includeStack) {ErrorMessage += '\\nStack:' + $.stack;}",
|
||||
" return ErrorMessage;",
|
||||
" })(e);",
|
||||
"}"
|
||||
].join('')
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
// deal with the replacements //
|
||||
// Note it's probably better to use ${template} `literals` //
|
||||
/////////////////////////////////////////////////////////////
|
||||
|
||||
if (replacements) {
|
||||
for (key in replacements) {
|
||||
if (replacements.hasOwnProperty(key)) {
|
||||
replaceThis = new RegExp('__' + key + '__', 'g');
|
||||
withThis = replacements[key];
|
||||
jsxScript = jsxScript.replace(replaceThis, withThis + '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
evalScript(jsxScript, callback);
|
||||
return true;
|
||||
} catch (err) {
|
||||
////////////////////////////////////////////////
|
||||
// Do whatever error handling you want here ! //
|
||||
////////////////////////////////////////////////
|
||||
var newErr;
|
||||
newErr = new Error(err);
|
||||
alert('Error Eek: ' + newErr.stack);
|
||||
return false;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* [evalFile] For calling jsx scripts from the js engine
|
||||
*
|
||||
* The jsx.evalFiles method is used for executing saved jsx scripts
|
||||
* where the jsxScript parameter is a string of the jsx scripts file location.
|
||||
* For convenience jsx.file or jsx.evalfile can be used instead of jsx.evalFile
|
||||
*
|
||||
* @param {String} file
|
||||
* The path to jsx script
|
||||
* If only the base name is provided then the path will be presumed to be the
|
||||
* To execute files stored in the jsx folder located in the __dirname folder use
|
||||
* jsx.evalFile('myFabJsxScript.jsx');
|
||||
* To execute files stored in the a folder myFabScripts located in the __dirname folder use
|
||||
* jsx.evalFile('./myFabScripts/myFabJsxScript.jsx');
|
||||
* To execute files stored in the a folder myFabScripts located at an absolute url use
|
||||
* jsx.evalFile('/Path/to/my/FabJsxScript.jsx'); (mac)
|
||||
* or jsx.evalFile('C:Path/to/my/FabJsxScript.jsx'); (windows)
|
||||
*
|
||||
* @param {Function} callback
|
||||
* The callback function you want the jsx script to trigger on completion
|
||||
* The result of the jsx script is passed as the argument to that function
|
||||
* The function can exist in some other file.
|
||||
* Note that InDesign does not automatically pass the callBack as a string.
|
||||
* Either write your InDesign in a way that it returns a sting the form of
|
||||
* return 'this is my result surrounded by quotes'
|
||||
* or use the force eval option
|
||||
* [Optional DEFAULT no callBack]
|
||||
*
|
||||
* @param {Object} replacements
|
||||
* The replacements to make on the jsx script
|
||||
* give the following script (template)
|
||||
* 'alert("__message__: " + __val__);'
|
||||
* and we want to change the script to
|
||||
* 'alert("I was born in the year: " + 1234);'
|
||||
* we would pass the following object
|
||||
* {"message": 'I was born in the year', "val": 1234}
|
||||
* or if not using reserved words like do we can leave out the key quotes
|
||||
* {message: 'I was born in the year', val: 1234}
|
||||
* By default when possible the forceEvalScript will be set to true
|
||||
* The forceEvalScript option cannot be true when there are replacements
|
||||
* To force the forceEvalScript to be false you can send a blank set of replacements
|
||||
* jsx.evalFile('myFabScript.jsx', {}); Will NOT be executed using the $.evalScript method
|
||||
* jsx.evalFile('myFabScript.jsx'); Will YES be executed using the $.evalScript method
|
||||
* see the forceEvalScript parameter for details on this
|
||||
* [Optional DEFAULT no replacements]
|
||||
*
|
||||
* @param {Bolean} forceEval
|
||||
* If the script should be wrapped in an eval and try catch
|
||||
* This will 1) provide useful error feedback if heaven forbid it is needed
|
||||
* 2) The result will be a string which is required for callback results in InDesign
|
||||
* [Optional DEFAULT true]
|
||||
*
|
||||
* If no replacements are needed then the jsx script is be executed by using the $.evalFile method
|
||||
* This exposes the true value of the $.fileName property <span class="wp-font-emots-emo-sunglasses"></span>
|
||||
* In such a case it's best to avoid using the $.__fileName() with no base name as it won't work
|
||||
* BUT one can still use the $.__fileName('baseName') method which is more accurate than the standard $.fileName property <span class="wp-font-emots-emo-happy"></span>
|
||||
* Let's say you have a Drive called "Graphics" AND YOU HAVE a root folder on your "main" drive called "Graphics"
|
||||
* You call a script jsx.evalFile('/Volumes/Graphics/myFabScript.jsx');
|
||||
* $.fileName will give you '/Graphics/myFabScript.jsx' which is wrong
|
||||
* $.__fileName('myFabScript.jsx') will give you '/Volumes/Graphics/myFabScript.jsx' which is correct
|
||||
* $.__fileName() will not give you a reliable result
|
||||
* Note that if your calling multiple versions of myFabScript.jsx stored in multiple folders then you can get stuffed!
|
||||
* i.e. if the fileName is important to you then don't do that.
|
||||
* It also will force the result of the jsx file as a string which is particularly useful for InDesign callBacks
|
||||
*
|
||||
* Note 1) The order of the parameters is irrelevant
|
||||
* Note 2) One can pass the arguments as an object if desired
|
||||
* jsx.evalScript(myCallBackFunction, 'alert("__myMessage__");', true);
|
||||
* is the same as
|
||||
* jsx.evalScript({
|
||||
* script: 'alert("__myMessage__");',
|
||||
* replacements: {myMessage: 'Hi there'},
|
||||
* callBack: myCallBackFunction,
|
||||
* eval: false,
|
||||
* });
|
||||
* note that either lower or camelCase key names or valid
|
||||
* i.e. both callback or callBack will work
|
||||
*
|
||||
* The following keys are the same file || jsx || script || jsxScript || jsxscript
|
||||
* The following keys are the same callBack || callback
|
||||
* The following keys are the same replacements || replace
|
||||
* The following keys are the same eval || forceEval || forceeval
|
||||
*
|
||||
* @return {Boolean} if the jsxScript was executed or not
|
||||
*/
|
||||
|
||||
Jsx.prototype.evalFile = function() {
|
||||
var arg, args, callback, fileName, fileNameScript, forceEval, forceEvalScript,
|
||||
i, jsxFolder, jsxScript, newLine, replacements, success;
|
||||
|
||||
success = true; // optimistic <span class="wp-font-emots-emo-happy"></span>
|
||||
args = arguments;
|
||||
|
||||
jsxFolder = path.join(__dirname, 'jsx');
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// $.fileName does not return it's correct path in the jsx engine for files called from the js engine //
|
||||
// In Illustrator it returns an integer in InDesign it returns an empty string //
|
||||
// This script injection allows for the script to know it's path by calling //
|
||||
// $.__fileName(); //
|
||||
// on Illustrator this works pretty well //
|
||||
// on InDesign it's best to use with a bit of care //
|
||||
// If the a second script has been called the InDesing will "forget" the path to the first script //
|
||||
// 2 work-arounds for this //
|
||||
// 1) at the beginning of your script add var thePathToMeIs = $.fileName(); //
|
||||
// thePathToMeIs will not be forgotten after running the second script //
|
||||
// 2) $.__fileName('myBaseName.jsx'); //
|
||||
// for example you have file with the following path //
|
||||
// /path/to/me.jsx //
|
||||
// Call $.__fileName('me.jsx') and you will get /path/to/me.jsx even after executing a second script //
|
||||
// Note When the forceEvalScript option is used then you just use the regular $.fileName property //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
fileNameScript = [
|
||||
// The if statement should not normally be executed
|
||||
'if(!$.__fileNames){',
|
||||
' $.__fileNames = {};',
|
||||
' $.__dirname = "__dirname__";'.replace('__dirname__', __dirname),
|
||||
' $.__fileName = function(name){',
|
||||
' name = name || $.fileName;',
|
||||
' return ($.__fileNames && $.__fileNames[name]) || $.fileName;',
|
||||
' };',
|
||||
'}',
|
||||
'$.__fileNames["__basename__"] = $.__fileNames["" + $.fileName] = "__fileName__";'
|
||||
].join('');
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// sort out order which arguments into jsxScript, callback, replacements, forceEval //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// Detect if the parameters were passed as an object and if so allow for various keys
|
||||
if (args.length === 1 && (arg = args[0]) instanceof Object) {
|
||||
jsxScript = arg.jsxScript || arg.jsx || arg.script || arg.file || arg.jsxscript;
|
||||
callback = arg.callBack || arg.callback;
|
||||
replacements = arg.replacements || arg.replace;
|
||||
forceEval = arg.eval || arg.forceEval || arg.forceeval;
|
||||
} else {
|
||||
for (i = 0; i < 5; i++) {
|
||||
arg = args[i];
|
||||
if (arg === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (arg.constructor.name === 'String') {
|
||||
jsxScript = arg;
|
||||
continue;
|
||||
}
|
||||
if (arg.constructor.name === 'Object') {
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// If no replacements are provided then the $.evalScript method will be used //
|
||||
// This will allow directly for the $.fileName property to be used //
|
||||
// If one does not want the $.evalScript method to be used then //
|
||||
// either send a blank object as the replacements {} //
|
||||
// or explicitly set the forceEvalScript option to false //
|
||||
// This can only be done if the parameters are passed as an object //
|
||||
// i.e. jsx.evalFile({file:'myFabScript.jsx', forceEvalScript: false}); //
|
||||
// if the file was called using //
|
||||
// i.e. jsx.evalFile('myFabScript.jsx'); //
|
||||
// then the following jsx code is called $.evalFile(new File('Path/to/myFabScript.jsx', 10000000000)) + ''; //
|
||||
// forceEval is never needed if the forceEvalScript is triggered //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
replacements = arg;
|
||||
continue;
|
||||
}
|
||||
if (arg.constructor === Function) {
|
||||
callback = arg;
|
||||
continue;
|
||||
}
|
||||
if (arg === false) {
|
||||
forceEval = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no script provide then not too much to do!
|
||||
if (!jsxScript) {
|
||||
return false;
|
||||
}
|
||||
|
||||
forceEvalScript = !replacements;
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////
|
||||
// Get path of script //
|
||||
// Check if it's literal, relative or in jsx folder //
|
||||
//////////////////////////////////////////////////////
|
||||
|
||||
if (/^\/|[a-zA-Z]+:/.test(jsxScript)) { // absolute path Mac | Windows
|
||||
jsxScript = path.normalize(jsxScript);
|
||||
} else if (/^\.+\//.test(jsxScript)) {
|
||||
jsxScript = path.join(__dirname, jsxScript); // relative path
|
||||
} else {
|
||||
jsxScript = path.join(jsxFolder, jsxScript); // files in the jsxFolder
|
||||
}
|
||||
|
||||
if (forceEvalScript) {
|
||||
jsxScript = jsxScript.replace(/"/g, '\\"');
|
||||
// Check that the path exist, should change this to asynchronous at some point
|
||||
if (!window.cep.fs.stat(jsxScript).err) {
|
||||
jsxScript = fileNameScript.replace(/__fileName__/, jsxScript).replace(/__basename__/, path.basename(jsxScript)) +
|
||||
'$.evalFile(new File("' + jsxScript.replace(/\\/g, '\\\\') + '")) + "";';
|
||||
return this.evalScript(jsxScript, callback, forceEval);
|
||||
} else {
|
||||
throw new Error(`The file: {jsxScript} could not be found / read`);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Replacements made so we can't use $.evalFile and need to read the jsx script for ourselves //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
fileName = jsxScript.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
try {
|
||||
jsxScript = window.cep.fs.readFile(jsxScript).data;
|
||||
} catch (er) {
|
||||
throw new Error(`The file: ${fileName} could not be read`);
|
||||
}
|
||||
// It is desirable that the injected fileNameScript is on the same line as the 1st line of the script
|
||||
// This is so that the $.line or error.line returns the same value as the actual file
|
||||
// However if the 1st line contains a # directive then we need to insert a new line and stuff the above problem
|
||||
// When possible i.e. when there's no replacements then $.evalFile will be used and then the whole issue is avoided
|
||||
newLine = /^\s*#/.test(jsxScript) ? '\n' : '';
|
||||
jsxScript = fileNameScript.replace(/__fileName__/, fileName).replace(/__basename__/, path.basename(fileName)) + newLine + jsxScript;
|
||||
|
||||
try {
|
||||
// evalScript(jsxScript, callback);
|
||||
return this.evalScript(jsxScript, callback, replacements, forceEval);
|
||||
} catch (err) {
|
||||
////////////////////////////////////////////////
|
||||
// Do whatever error handling you want here ! //
|
||||
////////////////////////////////////////////////
|
||||
var newErr;
|
||||
newErr = new Error(err);
|
||||
alert('Error Eek: ' + newErr.stack);
|
||||
return false;
|
||||
}
|
||||
|
||||
return success; // success should be an array but for now it's a Boolean
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////
|
||||
// Setup alternative method names //
|
||||
////////////////////////////////////
|
||||
Jsx.prototype.eval = Jsx.prototype.script = Jsx.prototype.evalscript = Jsx.prototype.evalScript;
|
||||
Jsx.prototype.file = Jsx.prototype.evalfile = Jsx.prototype.evalFile;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Examples //
|
||||
// jsx.evalScript('alert("foo");'); //
|
||||
// jsx.evalFile('foo.jsx'); // where foo.jsx is stored in the jsx folder at the base of the extensions directory //
|
||||
// jsx.evalFile('../myFolder/foo.jsx'); // where a relative or absolute file path is given //
|
||||
// //
|
||||
// using conventional methods one would use in the case were the values to swap were supplied by variables //
|
||||
// csInterface.evalScript('var q = "' + name + '"; alert("' + myString + '" ' + myOp + ' q);q;', callback); //
|
||||
// Using all the '' + foo + '' is very error prone //
|
||||
// jsx.evalScript('var q = "__name__"; alert(__string__ __opp__ q);q;',{'name':'Fred', 'string':'Hello ', 'opp':'+'}, callBack); //
|
||||
// is much simpler and less error prone //
|
||||
// //
|
||||
// more readable to use object //
|
||||
// jsx.evalFile({ //
|
||||
// file: 'yetAnotherFabScript.jsx', //
|
||||
// replacements: {"this": foo, That: bar, and: "&&", the: foo2, other: bar2}, //
|
||||
// eval: true //
|
||||
// }) //
|
||||
// Enjoy <span class="wp-font-emots-emo-happy"></span> //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
jsx = new Jsx();
|
||||
})();
|
||||
484
openpype/hosts/photoshop/api/extension/host/index.jsx
Normal file
484
openpype/hosts/photoshop/api/extension/host/index.jsx
Normal file
File diff suppressed because one or more lines are too long
530
openpype/hosts/photoshop/api/extension/host/json.js
Normal file
530
openpype/hosts/photoshop/api/extension/host/json.js
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
// json2.js
|
||||
// 2017-06-12
|
||||
// Public Domain.
|
||||
// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
||||
|
||||
// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
||||
// NOT CONTROL.
|
||||
|
||||
// This file creates a global JSON object containing two methods: stringify
|
||||
// and parse. This file provides the ES5 JSON capability to ES3 systems.
|
||||
// If a project might run on IE8 or earlier, then this file should be included.
|
||||
// This file does nothing on ES5 systems.
|
||||
|
||||
// JSON.stringify(value, replacer, space)
|
||||
// value any JavaScript value, usually an object or array.
|
||||
// replacer an optional parameter that determines how object
|
||||
// values are stringified for objects. It can be a
|
||||
// function or an array of strings.
|
||||
// space an optional parameter that specifies the indentation
|
||||
// of nested structures. If it is omitted, the text will
|
||||
// be packed without extra whitespace. If it is a number,
|
||||
// it will specify the number of spaces to indent at each
|
||||
// level. If it is a string (such as "\t" or " "),
|
||||
// it contains the characters used to indent at each level.
|
||||
// This method produces a JSON text from a JavaScript value.
|
||||
// When an object value is found, if the object contains a toJSON
|
||||
// method, its toJSON method will be called and the result will be
|
||||
// stringified. A toJSON method does not serialize: it returns the
|
||||
// value represented by the name/value pair that should be serialized,
|
||||
// or undefined if nothing should be serialized. The toJSON method
|
||||
// will be passed the key associated with the value, and this will be
|
||||
// bound to the value.
|
||||
|
||||
// For example, this would serialize Dates as ISO strings.
|
||||
|
||||
// Date.prototype.toJSON = function (key) {
|
||||
// function f(n) {
|
||||
// // Format integers to have at least two digits.
|
||||
// return (n < 10)
|
||||
// ? "0" + n
|
||||
// : n;
|
||||
// }
|
||||
// return this.getUTCFullYear() + "-" +
|
||||
// f(this.getUTCMonth() + 1) + "-" +
|
||||
// f(this.getUTCDate()) + "T" +
|
||||
// f(this.getUTCHours()) + ":" +
|
||||
// f(this.getUTCMinutes()) + ":" +
|
||||
// f(this.getUTCSeconds()) + "Z";
|
||||
// };
|
||||
|
||||
// You can provide an optional replacer method. It will be passed the
|
||||
// key and value of each member, with this bound to the containing
|
||||
// object. The value that is returned from your method will be
|
||||
// serialized. If your method returns undefined, then the member will
|
||||
// be excluded from the serialization.
|
||||
|
||||
// If the replacer parameter is an array of strings, then it will be
|
||||
// used to select the members to be serialized. It filters the results
|
||||
// such that only members with keys listed in the replacer array are
|
||||
// stringified.
|
||||
|
||||
// Values that do not have JSON representations, such as undefined or
|
||||
// functions, will not be serialized. Such values in objects will be
|
||||
// dropped; in arrays they will be replaced with null. You can use
|
||||
// a replacer function to replace those with JSON values.
|
||||
|
||||
// JSON.stringify(undefined) returns undefined.
|
||||
|
||||
// The optional space parameter produces a stringification of the
|
||||
// value that is filled with line breaks and indentation to make it
|
||||
// easier to read.
|
||||
|
||||
// If the space parameter is a non-empty string, then that string will
|
||||
// be used for indentation. If the space parameter is a number, then
|
||||
// the indentation will be that many spaces.
|
||||
|
||||
// Example:
|
||||
|
||||
// text = JSON.stringify(["e", {pluribus: "unum"}]);
|
||||
// // text is '["e",{"pluribus":"unum"}]'
|
||||
|
||||
// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t");
|
||||
// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
|
||||
|
||||
// text = JSON.stringify([new Date()], function (key, value) {
|
||||
// return this[key] instanceof Date
|
||||
// ? "Date(" + this[key] + ")"
|
||||
// : value;
|
||||
// });
|
||||
// // text is '["Date(---current time---)"]'
|
||||
|
||||
// JSON.parse(text, reviver)
|
||||
// This method parses a JSON text to produce an object or array.
|
||||
// It can throw a SyntaxError exception.
|
||||
|
||||
// The optional reviver parameter is a function that can filter and
|
||||
// transform the results. It receives each of the keys and values,
|
||||
// and its return value is used instead of the original value.
|
||||
// If it returns what it received, then the structure is not modified.
|
||||
// If it returns undefined then the member is deleted.
|
||||
|
||||
// Example:
|
||||
|
||||
// // Parse the text. Values that look like ISO date strings will
|
||||
// // be converted to Date objects.
|
||||
|
||||
// myData = JSON.parse(text, function (key, value) {
|
||||
// var a;
|
||||
// if (typeof value === "string") {
|
||||
// a =
|
||||
// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
|
||||
// if (a) {
|
||||
// return new Date(Date.UTC(
|
||||
// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]
|
||||
// ));
|
||||
// }
|
||||
// return value;
|
||||
// }
|
||||
// });
|
||||
|
||||
// myData = JSON.parse(
|
||||
// "[\"Date(09/09/2001)\"]",
|
||||
// function (key, value) {
|
||||
// var d;
|
||||
// if (
|
||||
// typeof value === "string"
|
||||
// && value.slice(0, 5) === "Date("
|
||||
// && value.slice(-1) === ")"
|
||||
// ) {
|
||||
// d = new Date(value.slice(5, -1));
|
||||
// if (d) {
|
||||
// return d;
|
||||
// }
|
||||
// }
|
||||
// return value;
|
||||
// }
|
||||
// );
|
||||
|
||||
// This is a reference implementation. You are free to copy, modify, or
|
||||
// redistribute.
|
||||
|
||||
/*jslint
|
||||
eval, for, this
|
||||
*/
|
||||
|
||||
/*property
|
||||
JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
|
||||
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
|
||||
lastIndex, length, parse, prototype, push, replace, slice, stringify,
|
||||
test, toJSON, toString, valueOf
|
||||
*/
|
||||
|
||||
|
||||
// Create a JSON object only if one does not already exist. We create the
|
||||
// methods in a closure to avoid creating global variables.
|
||||
|
||||
if (typeof JSON !== "object") {
|
||||
JSON = {};
|
||||
}
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var rx_one = /^[\],:{}\s]*$/;
|
||||
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
|
||||
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
|
||||
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
|
||||
var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
||||
var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
||||
|
||||
function f(n) {
|
||||
// Format integers to have at least two digits.
|
||||
return (n < 10)
|
||||
? "0" + n
|
||||
: n;
|
||||
}
|
||||
|
||||
function this_value() {
|
||||
return this.valueOf();
|
||||
}
|
||||
|
||||
if (typeof Date.prototype.toJSON !== "function") {
|
||||
|
||||
Date.prototype.toJSON = function () {
|
||||
|
||||
return isFinite(this.valueOf())
|
||||
? (
|
||||
this.getUTCFullYear()
|
||||
+ "-"
|
||||
+ f(this.getUTCMonth() + 1)
|
||||
+ "-"
|
||||
+ f(this.getUTCDate())
|
||||
+ "T"
|
||||
+ f(this.getUTCHours())
|
||||
+ ":"
|
||||
+ f(this.getUTCMinutes())
|
||||
+ ":"
|
||||
+ f(this.getUTCSeconds())
|
||||
+ "Z"
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
||||
Boolean.prototype.toJSON = this_value;
|
||||
Number.prototype.toJSON = this_value;
|
||||
String.prototype.toJSON = this_value;
|
||||
}
|
||||
|
||||
var gap;
|
||||
var indent;
|
||||
var meta;
|
||||
var rep;
|
||||
|
||||
|
||||
function quote(string) {
|
||||
|
||||
// If the string contains no control characters, no quote characters, and no
|
||||
// backslash characters, then we can safely slap some quotes around it.
|
||||
// Otherwise we must also replace the offending characters with safe escape
|
||||
// sequences.
|
||||
|
||||
rx_escapable.lastIndex = 0;
|
||||
return rx_escapable.test(string)
|
||||
? "\"" + string.replace(rx_escapable, function (a) {
|
||||
var c = meta[a];
|
||||
return typeof c === "string"
|
||||
? c
|
||||
: "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
|
||||
}) + "\""
|
||||
: "\"" + string + "\"";
|
||||
}
|
||||
|
||||
|
||||
function str(key, holder) {
|
||||
|
||||
// Produce a string from holder[key].
|
||||
|
||||
var i; // The loop counter.
|
||||
var k; // The member key.
|
||||
var v; // The member value.
|
||||
var length;
|
||||
var mind = gap;
|
||||
var partial;
|
||||
var value = holder[key];
|
||||
|
||||
// If the value has a toJSON method, call it to obtain a replacement value.
|
||||
|
||||
if (
|
||||
value
|
||||
&& typeof value === "object"
|
||||
&& typeof value.toJSON === "function"
|
||||
) {
|
||||
value = value.toJSON(key);
|
||||
}
|
||||
|
||||
// If we were called with a replacer function, then call the replacer to
|
||||
// obtain a replacement value.
|
||||
|
||||
if (typeof rep === "function") {
|
||||
value = rep.call(holder, key, value);
|
||||
}
|
||||
|
||||
// What happens next depends on the value's type.
|
||||
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
return quote(value);
|
||||
|
||||
case "number":
|
||||
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
|
||||
return (isFinite(value))
|
||||
? String(value)
|
||||
: "null";
|
||||
|
||||
case "boolean":
|
||||
case "null":
|
||||
|
||||
// If the value is a boolean or null, convert it to a string. Note:
|
||||
// typeof null does not produce "null". The case is included here in
|
||||
// the remote chance that this gets fixed someday.
|
||||
|
||||
return String(value);
|
||||
|
||||
// If the type is "object", we might be dealing with an object or an array or
|
||||
// null.
|
||||
|
||||
case "object":
|
||||
|
||||
// Due to a specification blunder in ECMAScript, typeof null is "object",
|
||||
// so watch out for that case.
|
||||
|
||||
if (!value) {
|
||||
return "null";
|
||||
}
|
||||
|
||||
// Make an array to hold the partial results of stringifying this object value.
|
||||
|
||||
gap += indent;
|
||||
partial = [];
|
||||
|
||||
// Is the value an array?
|
||||
|
||||
if (Object.prototype.toString.apply(value) === "[object Array]") {
|
||||
|
||||
// The value is an array. Stringify every element. Use null as a placeholder
|
||||
// for non-JSON values.
|
||||
|
||||
length = value.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
partial[i] = str(i, value) || "null";
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap them in
|
||||
// brackets.
|
||||
|
||||
v = partial.length === 0
|
||||
? "[]"
|
||||
: gap
|
||||
? (
|
||||
"[\n"
|
||||
+ gap
|
||||
+ partial.join(",\n" + gap)
|
||||
+ "\n"
|
||||
+ mind
|
||||
+ "]"
|
||||
)
|
||||
: "[" + partial.join(",") + "]";
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
|
||||
// If the replacer is an array, use it to select the members to be stringified.
|
||||
|
||||
if (rep && typeof rep === "object") {
|
||||
length = rep.length;
|
||||
for (i = 0; i < length; i += 1) {
|
||||
if (typeof rep[i] === "string") {
|
||||
k = rep[i];
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (
|
||||
(gap)
|
||||
? ": "
|
||||
: ":"
|
||||
) + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Otherwise, iterate through all of the keys in the object.
|
||||
|
||||
for (k in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||
v = str(k, value);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (
|
||||
(gap)
|
||||
? ": "
|
||||
: ":"
|
||||
) + v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
|
||||
v = partial.length === 0
|
||||
? "{}"
|
||||
: gap
|
||||
? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}"
|
||||
: "{" + partial.join(",") + "}";
|
||||
gap = mind;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// If the JSON object does not yet have a stringify method, give it one.
|
||||
|
||||
if (typeof JSON.stringify !== "function") {
|
||||
meta = { // table of character substitutions
|
||||
"\b": "\\b",
|
||||
"\t": "\\t",
|
||||
"\n": "\\n",
|
||||
"\f": "\\f",
|
||||
"\r": "\\r",
|
||||
"\"": "\\\"",
|
||||
"\\": "\\\\"
|
||||
};
|
||||
JSON.stringify = function (value, replacer, space) {
|
||||
|
||||
// The stringify method takes a value and an optional replacer, and an optional
|
||||
// space parameter, and returns a JSON text. The replacer can be a function
|
||||
// that can replace values, or an array of strings that will select the keys.
|
||||
// A default replacer method can be provided. Use of the space parameter can
|
||||
// produce text that is more easily readable.
|
||||
|
||||
var i;
|
||||
gap = "";
|
||||
indent = "";
|
||||
|
||||
// If the space parameter is a number, make an indent string containing that
|
||||
// many spaces.
|
||||
|
||||
if (typeof space === "number") {
|
||||
for (i = 0; i < space; i += 1) {
|
||||
indent += " ";
|
||||
}
|
||||
|
||||
// If the space parameter is a string, it will be used as the indent string.
|
||||
|
||||
} else if (typeof space === "string") {
|
||||
indent = space;
|
||||
}
|
||||
|
||||
// If there is a replacer, it must be a function or an array.
|
||||
// Otherwise, throw an error.
|
||||
|
||||
rep = replacer;
|
||||
if (replacer && typeof replacer !== "function" && (
|
||||
typeof replacer !== "object"
|
||||
|| typeof replacer.length !== "number"
|
||||
)) {
|
||||
throw new Error("JSON.stringify");
|
||||
}
|
||||
|
||||
// Make a fake root object containing our value under the key of "".
|
||||
// Return the result of stringifying the value.
|
||||
|
||||
return str("", {"": value});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If the JSON object does not yet have a parse method, give it one.
|
||||
|
||||
if (typeof JSON.parse !== "function") {
|
||||
JSON.parse = function (text, reviver) {
|
||||
|
||||
// The parse method takes a text and an optional reviver function, and returns
|
||||
// a JavaScript value if the text is a valid JSON text.
|
||||
|
||||
var j;
|
||||
|
||||
function walk(holder, key) {
|
||||
|
||||
// The walk method is used to recursively walk the resulting structure so
|
||||
// that modifications can be made.
|
||||
|
||||
var k;
|
||||
var v;
|
||||
var value = holder[key];
|
||||
if (value && typeof value === "object") {
|
||||
for (k in value) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, k)) {
|
||||
v = walk(value, k);
|
||||
if (v !== undefined) {
|
||||
value[k] = v;
|
||||
} else {
|
||||
delete value[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return reviver.call(holder, key, value);
|
||||
}
|
||||
|
||||
|
||||
// Parsing happens in four stages. In the first stage, we replace certain
|
||||
// Unicode characters with escape sequences. JavaScript handles many characters
|
||||
// incorrectly, either silently deleting them, or treating them as line endings.
|
||||
|
||||
text = String(text);
|
||||
rx_dangerous.lastIndex = 0;
|
||||
if (rx_dangerous.test(text)) {
|
||||
text = text.replace(rx_dangerous, function (a) {
|
||||
return (
|
||||
"\\u"
|
||||
+ ("0000" + a.charCodeAt(0).toString(16)).slice(-4)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// In the second stage, we run the text against regular expressions that look
|
||||
// for non-JSON patterns. We are especially concerned with "()" and "new"
|
||||
// because they can cause invocation, and "=" because it can cause mutation.
|
||||
// But just to be safe, we want to reject all unexpected forms.
|
||||
|
||||
// We split the second stage into 4 regexp operations in order to work around
|
||||
// crippling inefficiencies in IE's and Safari's regexp engines. First we
|
||||
// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
|
||||
// replace all simple value tokens with "]" characters. Third, we delete all
|
||||
// open brackets that follow a colon or comma or that begin the text. Finally,
|
||||
// we look to see that the remaining characters are only whitespace or "]" or
|
||||
// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.
|
||||
|
||||
if (
|
||||
rx_one.test(
|
||||
text
|
||||
.replace(rx_two, "@")
|
||||
.replace(rx_three, "]")
|
||||
.replace(rx_four, "")
|
||||
)
|
||||
) {
|
||||
|
||||
// In the third stage we use the eval function to compile the text into a
|
||||
// JavaScript structure. The "{" operator is subject to a syntactic ambiguity
|
||||
// in JavaScript: it can begin a block or an object literal. We wrap the text
|
||||
// in parens to eliminate the ambiguity.
|
||||
|
||||
j = eval("(" + text + ")");
|
||||
|
||||
// In the optional fourth stage, we recursively walk the new structure, passing
|
||||
// each name/value pair to a reviver function for possible transformation.
|
||||
|
||||
return (typeof reviver === "function")
|
||||
? walk({"": j}, "")
|
||||
: j;
|
||||
}
|
||||
|
||||
// If the text is not JSON parseable, then a SyntaxError is thrown.
|
||||
|
||||
throw new SyntaxError("JSON.parse");
|
||||
};
|
||||
}
|
||||
}());
|
||||
BIN
openpype/hosts/photoshop/api/extension/icons/avalon-logo-48.png
Normal file
BIN
openpype/hosts/photoshop/api/extension/icons/avalon-logo-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
119
openpype/hosts/photoshop/api/extension/index.html
Normal file
119
openpype/hosts/photoshop/api/extension/index.html
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style type="text/css">
|
||||
html, body, iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px;
|
||||
margin: 0px;
|
||||
overflow: hidden;
|
||||
background-color: #424242;
|
||||
}
|
||||
button {width: 100%;}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
button {width: 100%;}
|
||||
body {margin:0; padding:0; height: 100%;}
|
||||
html {height: 100%;}
|
||||
</style>
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#workfiles-button").bind("click", function() {
|
||||
RPC.call('Photoshop.workfiles_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#creator-button").bind("click", function() {
|
||||
RPC.call('Photoshop.creator_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#loader-button").bind("click", function() {
|
||||
RPC.call('Photoshop.loader_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#publish-button").bind("click", function() {
|
||||
RPC.call('Photoshop.publish_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#sceneinventory-button").bind("click", function() {
|
||||
RPC.call('Photoshop.sceneinventory_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#subsetmanager-button").bind("click", function() {
|
||||
RPC.call('Photoshop.subsetmanager_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type=text/javascript>
|
||||
$(function() {
|
||||
$("a#experimental-button").bind("click", function() {
|
||||
RPC.call('Photoshop.experimental_tools_route').then(function (data) {
|
||||
}, function (error) {
|
||||
alert(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="./client/wsrpc.js"></script>
|
||||
<script type="text/javascript" src="./client/CSInterface.js"></script>
|
||||
<script type="text/javascript" src="./client/loglevel.min.js"></script>
|
||||
|
||||
<!-- helper library for better debugging of .jsx check its license! -->
|
||||
<script type="text/javascript" src="./host/JSX.js"></script>
|
||||
|
||||
<script type="text/javascript" src="./client/client.js"></script>
|
||||
|
||||
<a href=# id=workfiles-button><button>Workfiles...</button></a>
|
||||
<a href=# id=creator-button><button>Create...</button></a>
|
||||
<a href=# id=loader-button><button>Load...</button></a>
|
||||
<a href=# id=publish-button><button>Publish...</button></a>
|
||||
<a href=# id=sceneinventory-button><button>Manage...</button></a>
|
||||
<a href=# id=subsetmanager-button><button>Subset Manager...</button></a>
|
||||
<a href=# id=experimental-button><button>Experimental Tools...</button></a>
|
||||
</body>
|
||||
</html>
|
||||
365
openpype/hosts/photoshop/api/launch_logic.py
Normal file
365
openpype/hosts/photoshop/api/launch_logic.py
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
import os
|
||||
import subprocess
|
||||
import collections
|
||||
import asyncio
|
||||
|
||||
from wsrpc_aiohttp import (
|
||||
WebSocketRoute,
|
||||
WebSocketAsync
|
||||
)
|
||||
|
||||
from Qt import QtCore
|
||||
|
||||
from openpype.api import Logger
|
||||
from openpype.tools.utils import host_tools
|
||||
|
||||
from avalon import api
|
||||
from avalon.tools.webserver.app import WebServerTool
|
||||
|
||||
from .ws_stub import PhotoshopServerStub
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
class ConnectionNotEstablishedYet(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MainThreadItem:
|
||||
"""Structure to store information about callback in main thread.
|
||||
|
||||
Item should be used to execute callback in main thread which may be needed
|
||||
for execution of Qt objects.
|
||||
|
||||
Item store callback (callable variable), arguments and keyword arguments
|
||||
for the callback. Item hold information about it's process.
|
||||
"""
|
||||
not_set = object()
|
||||
|
||||
def __init__(self, callback, *args, **kwargs):
|
||||
self._done = False
|
||||
self._exception = self.not_set
|
||||
self._result = self.not_set
|
||||
self._callback = callback
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
@property
|
||||
def done(self):
|
||||
return self._done
|
||||
|
||||
@property
|
||||
def exception(self):
|
||||
return self._exception
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
return self._result
|
||||
|
||||
def execute(self):
|
||||
"""Execute callback and store it's result.
|
||||
|
||||
Method must be called from main thread. Item is marked as `done`
|
||||
when callback execution finished. Store output of callback of exception
|
||||
information when callback raise one.
|
||||
"""
|
||||
log.debug("Executing process in main thread")
|
||||
if self.done:
|
||||
log.warning("- item is already processed")
|
||||
return
|
||||
|
||||
log.info("Running callback: {}".format(str(self._callback)))
|
||||
try:
|
||||
result = self._callback(*self._args, **self._kwargs)
|
||||
self._result = result
|
||||
|
||||
except Exception as exc:
|
||||
self._exception = exc
|
||||
|
||||
finally:
|
||||
self._done = True
|
||||
|
||||
|
||||
def stub():
|
||||
"""
|
||||
Convenience function to get server RPC stub to call methods directed
|
||||
for host (Photoshop).
|
||||
It expects already created connection, started from client.
|
||||
Currently created when panel is opened (PS: Window>Extensions>Avalon)
|
||||
:return: <PhotoshopClientStub> where functions could be called from
|
||||
"""
|
||||
ps_stub = PhotoshopServerStub()
|
||||
if not ps_stub.client:
|
||||
raise ConnectionNotEstablishedYet("Connection is not created yet")
|
||||
|
||||
return ps_stub
|
||||
|
||||
|
||||
def show_tool_by_name(tool_name):
|
||||
kwargs = {}
|
||||
if tool_name == "loader":
|
||||
kwargs["use_context"] = True
|
||||
|
||||
host_tools.show_tool_by_name(tool_name, **kwargs)
|
||||
|
||||
|
||||
class ProcessLauncher(QtCore.QObject):
|
||||
route_name = "Photoshop"
|
||||
_main_thread_callbacks = collections.deque()
|
||||
|
||||
def __init__(self, subprocess_args):
|
||||
self._subprocess_args = subprocess_args
|
||||
self._log = None
|
||||
|
||||
super(ProcessLauncher, self).__init__()
|
||||
|
||||
# Keep track if launcher was already started
|
||||
self._started = False
|
||||
|
||||
self._process = None
|
||||
self._websocket_server = None
|
||||
|
||||
start_process_timer = QtCore.QTimer()
|
||||
start_process_timer.setInterval(100)
|
||||
|
||||
loop_timer = QtCore.QTimer()
|
||||
loop_timer.setInterval(200)
|
||||
|
||||
start_process_timer.timeout.connect(self._on_start_process_timer)
|
||||
loop_timer.timeout.connect(self._on_loop_timer)
|
||||
|
||||
self._start_process_timer = start_process_timer
|
||||
self._loop_timer = loop_timer
|
||||
|
||||
@property
|
||||
def log(self):
|
||||
if self._log is None:
|
||||
self._log = Logger.get_logger(
|
||||
"{}-launcher".format(self.route_name)
|
||||
)
|
||||
return self._log
|
||||
|
||||
@property
|
||||
def websocket_server_is_running(self):
|
||||
if self._websocket_server is not None:
|
||||
return self._websocket_server.is_running
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_process_running(self):
|
||||
if self._process is not None:
|
||||
return self._process.poll() is None
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_host_connected(self):
|
||||
"""Returns True if connected, False if app is not running at all."""
|
||||
if not self.is_process_running:
|
||||
return False
|
||||
|
||||
try:
|
||||
_stub = stub()
|
||||
if _stub:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def execute_in_main_thread(cls, callback, *args, **kwargs):
|
||||
item = MainThreadItem(callback, *args, **kwargs)
|
||||
cls._main_thread_callbacks.append(item)
|
||||
return item
|
||||
|
||||
def start(self):
|
||||
if self._started:
|
||||
return
|
||||
self.log.info("Started launch logic of AfterEffects")
|
||||
self._started = True
|
||||
self._start_process_timer.start()
|
||||
|
||||
def exit(self):
|
||||
""" Exit whole application. """
|
||||
if self._start_process_timer.isActive():
|
||||
self._start_process_timer.stop()
|
||||
if self._loop_timer.isActive():
|
||||
self._loop_timer.stop()
|
||||
|
||||
if self._websocket_server is not None:
|
||||
self._websocket_server.stop()
|
||||
|
||||
if self._process:
|
||||
self._process.kill()
|
||||
self._process.wait()
|
||||
|
||||
QtCore.QCoreApplication.exit()
|
||||
|
||||
def _on_loop_timer(self):
|
||||
# TODO find better way and catch errors
|
||||
# Run only callbacks that are in queue at the moment
|
||||
cls = self.__class__
|
||||
for _ in range(len(cls._main_thread_callbacks)):
|
||||
if cls._main_thread_callbacks:
|
||||
item = cls._main_thread_callbacks.popleft()
|
||||
item.execute()
|
||||
|
||||
if not self.is_process_running:
|
||||
self.log.info("Host process is not running. Closing")
|
||||
self.exit()
|
||||
|
||||
elif not self.websocket_server_is_running:
|
||||
self.log.info("Websocket server is not running. Closing")
|
||||
self.exit()
|
||||
|
||||
def _on_start_process_timer(self):
|
||||
# TODO add try except validations for each part in this method
|
||||
# Start server as first thing
|
||||
if self._websocket_server is None:
|
||||
self._init_server()
|
||||
return
|
||||
|
||||
# TODO add waiting time
|
||||
# Wait for webserver
|
||||
if not self.websocket_server_is_running:
|
||||
return
|
||||
|
||||
# Start application process
|
||||
if self._process is None:
|
||||
self._start_process()
|
||||
self.log.info("Waiting for host to connect")
|
||||
return
|
||||
|
||||
# TODO add waiting time
|
||||
# Wait until host is connected
|
||||
if self.is_host_connected:
|
||||
self._start_process_timer.stop()
|
||||
self._loop_timer.start()
|
||||
elif (
|
||||
not self.is_process_running
|
||||
or not self.websocket_server_is_running
|
||||
):
|
||||
self.exit()
|
||||
|
||||
def _init_server(self):
|
||||
if self._websocket_server is not None:
|
||||
return
|
||||
|
||||
self.log.debug(
|
||||
"Initialization of websocket server for host communication"
|
||||
)
|
||||
|
||||
self._websocket_server = websocket_server = WebServerTool()
|
||||
if websocket_server.port_occupied(
|
||||
websocket_server.host_name,
|
||||
websocket_server.port
|
||||
):
|
||||
self.log.info(
|
||||
"Server already running, sending actual context and exit."
|
||||
)
|
||||
asyncio.run(websocket_server.send_context_change(self.route_name))
|
||||
self.exit()
|
||||
return
|
||||
|
||||
# Add Websocket route
|
||||
websocket_server.add_route("*", "/ws/", WebSocketAsync)
|
||||
# Add after effects route to websocket handler
|
||||
|
||||
print("Adding {} route".format(self.route_name))
|
||||
WebSocketAsync.add_route(
|
||||
self.route_name, PhotoshopRoute
|
||||
)
|
||||
self.log.info("Starting websocket server for host communication")
|
||||
websocket_server.start_server()
|
||||
|
||||
def _start_process(self):
|
||||
if self._process is not None:
|
||||
return
|
||||
self.log.info("Starting host process")
|
||||
try:
|
||||
self._process = subprocess.Popen(
|
||||
self._subprocess_args,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
except Exception:
|
||||
self.log.info("exce", exc_info=True)
|
||||
self.exit()
|
||||
|
||||
|
||||
class PhotoshopRoute(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 set_context(self, project, asset, task):
|
||||
"""
|
||||
Sets 'project' and 'asset' to envs, eg. setting context
|
||||
|
||||
Args:
|
||||
project (str)
|
||||
asset (str)
|
||||
"""
|
||||
log.info("Setting context change")
|
||||
log.info("project {} asset {} ".format(project, asset))
|
||||
if project:
|
||||
api.Session["AVALON_PROJECT"] = project
|
||||
os.environ["AVALON_PROJECT"] = project
|
||||
if asset:
|
||||
api.Session["AVALON_ASSET"] = asset
|
||||
os.environ["AVALON_ASSET"] = asset
|
||||
if task:
|
||||
api.Session["AVALON_TASK"] = task
|
||||
os.environ["AVALON_TASK"] = task
|
||||
|
||||
async def read(self):
|
||||
log.debug("photoshop.read client calls server server calls "
|
||||
"photoshop 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 subsetmanager_route(self):
|
||||
self._tool_route("subsetmanager")
|
||||
|
||||
async def experimental_tools_route(self):
|
||||
self._tool_route("experimental_tools")
|
||||
|
||||
def _tool_route(self, _tool_name):
|
||||
"""The address accessed when clicking on the buttons."""
|
||||
|
||||
ProcessLauncher.execute_in_main_thread(show_tool_by_name, _tool_name)
|
||||
|
||||
# Required return statement.
|
||||
return "nothing"
|
||||
78
openpype/hosts/photoshop/api/lib.py
Normal file
78
openpype/hosts/photoshop/api/lib.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import os
|
||||
import sys
|
||||
import contextlib
|
||||
import traceback
|
||||
|
||||
from Qt import QtWidgets
|
||||
|
||||
import avalon.api
|
||||
|
||||
from openpype.api import Logger
|
||||
from openpype.tools.utils import host_tools
|
||||
from openpype.lib.remote_publish import headless_publish
|
||||
|
||||
from .launch_logic import ProcessLauncher, stub
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
|
||||
def safe_excepthook(*args):
|
||||
traceback.print_exception(*args)
|
||||
|
||||
|
||||
def main(*subprocess_args):
|
||||
from openpype.hosts.photoshop import api
|
||||
|
||||
avalon.api.install(api)
|
||||
sys.excepthook = safe_excepthook
|
||||
|
||||
# coloring in ConsoleTrayApp
|
||||
os.environ["OPENPYPE_LOG_NO_COLORS"] = "False"
|
||||
app = QtWidgets.QApplication([])
|
||||
app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
launcher = ProcessLauncher(subprocess_args)
|
||||
launcher.start()
|
||||
|
||||
if os.environ.get("HEADLESS_PUBLISH"):
|
||||
launcher.execute_in_main_thread(
|
||||
headless_publish,
|
||||
log,
|
||||
"ClosePS",
|
||||
os.environ.get("IS_TEST")
|
||||
)
|
||||
elif os.environ.get("AVALON_PHOTOSHOP_WORKFILES_ON_LAUNCH", True):
|
||||
save = False
|
||||
if os.getenv("WORKFILES_SAVE_AS"):
|
||||
save = True
|
||||
|
||||
launcher.execute_in_main_thread(
|
||||
host_tools.show_workfiles, save=save
|
||||
)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_selection():
|
||||
"""Maintain selection during context."""
|
||||
selection = stub().get_selected_layers()
|
||||
try:
|
||||
yield selection
|
||||
finally:
|
||||
stub().select_layers(selection)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def maintained_visibility():
|
||||
"""Maintain visibility during context."""
|
||||
visibility = {}
|
||||
layers = stub().get_layers()
|
||||
for layer in layers:
|
||||
visibility[layer.id] = layer.visible
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for layer in layers:
|
||||
stub().set_visible(layer.id, visibility[layer.id])
|
||||
pass
|
||||
BIN
openpype/hosts/photoshop/api/panel.PNG
Normal file
BIN
openpype/hosts/photoshop/api/panel.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
openpype/hosts/photoshop/api/panel_failure.PNG
Normal file
BIN
openpype/hosts/photoshop/api/panel_failure.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
229
openpype/hosts/photoshop/api/pipeline.py
Normal file
229
openpype/hosts/photoshop/api/pipeline.py
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import os
|
||||
import sys
|
||||
from Qt import QtWidgets
|
||||
|
||||
import pyblish.api
|
||||
import avalon.api
|
||||
from avalon import pipeline, io
|
||||
|
||||
from openpype.api import Logger
|
||||
import openpype.hosts.photoshop
|
||||
|
||||
from . import lib
|
||||
|
||||
log = Logger.get_logger(__name__)
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.photoshop.__file__))
|
||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
|
||||
def check_inventory():
|
||||
if not lib.any_outdated():
|
||||
return
|
||||
|
||||
host = avalon.api.registered_host()
|
||||
outdated_containers = []
|
||||
for container in host.ls():
|
||||
representation = container['representation']
|
||||
representation_doc = io.find_one(
|
||||
{
|
||||
"_id": io.ObjectId(representation),
|
||||
"type": "representation"
|
||||
},
|
||||
projection={"parent": True}
|
||||
)
|
||||
if representation_doc and not lib.is_latest(representation_doc):
|
||||
outdated_containers.append(container)
|
||||
|
||||
# Warn about outdated containers.
|
||||
print("Starting new QApplication..")
|
||||
|
||||
message_box = QtWidgets.QMessageBox()
|
||||
message_box.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
msg = "There are outdated containers in the scene."
|
||||
message_box.setText(msg)
|
||||
message_box.exec_()
|
||||
|
||||
|
||||
def on_application_launch():
|
||||
check_inventory()
|
||||
|
||||
|
||||
def on_pyblish_instance_toggled(instance, old_value, new_value):
|
||||
"""Toggle layer visibility on instance toggles."""
|
||||
instance[0].Visible = new_value
|
||||
|
||||
|
||||
def install():
|
||||
"""Install Photoshop-specific functionality of avalon-core.
|
||||
|
||||
This function is called automatically on calling `api.install(photoshop)`.
|
||||
"""
|
||||
log.info("Installing OpenPype Photoshop...")
|
||||
pyblish.api.register_host("photoshop")
|
||||
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
|
||||
avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
|
||||
log.info(PUBLISH_PATH)
|
||||
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled
|
||||
)
|
||||
|
||||
avalon.api.on("application.launched", on_application_launch)
|
||||
|
||||
|
||||
def uninstall():
|
||||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||
avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
|
||||
avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
|
||||
|
||||
|
||||
def ls():
|
||||
"""Yields containers from active Photoshop document
|
||||
|
||||
This is the host-equivalent of api.ls(), but instead of listing
|
||||
assets on disk, it lists assets already loaded in Photoshop; once loaded
|
||||
they are called 'containers'
|
||||
|
||||
Yields:
|
||||
dict: container
|
||||
|
||||
"""
|
||||
try:
|
||||
stub = lib.stub() # only after Photoshop is up
|
||||
except lib.ConnectionNotEstablishedYet:
|
||||
print("Not connected yet, ignoring")
|
||||
return
|
||||
|
||||
if not stub.get_active_document_name():
|
||||
return
|
||||
|
||||
layers_meta = stub.get_layers_metadata() # minimalize calls to PS
|
||||
for layer in stub.get_layers():
|
||||
data = stub.read(layer, layers_meta)
|
||||
|
||||
# Skip non-tagged layers.
|
||||
if not data:
|
||||
continue
|
||||
|
||||
# Filter to only containers.
|
||||
if "container" not in data["id"]:
|
||||
continue
|
||||
|
||||
# Append transient data
|
||||
data["objectName"] = layer.name.replace(stub.LOADED_ICON, '')
|
||||
data["layer"] = layer
|
||||
|
||||
yield data
|
||||
|
||||
|
||||
def list_instances():
|
||||
"""List all created instances to publish from current workfile.
|
||||
|
||||
Pulls from File > File Info
|
||||
|
||||
For SubsetManager
|
||||
|
||||
Returns:
|
||||
(list) of dictionaries matching instances format
|
||||
"""
|
||||
stub = _get_stub()
|
||||
|
||||
if not stub:
|
||||
return []
|
||||
|
||||
instances = []
|
||||
layers_meta = stub.get_layers_metadata()
|
||||
if layers_meta:
|
||||
for key, instance in layers_meta.items():
|
||||
schema = instance.get("schema")
|
||||
if schema and "container" in schema:
|
||||
continue
|
||||
|
||||
instance['uuid'] = key
|
||||
instances.append(instance)
|
||||
|
||||
return instances
|
||||
|
||||
|
||||
def remove_instance(instance):
|
||||
"""Remove instance from current workfile metadata.
|
||||
|
||||
Updates metadata of current file in File > File Info and removes
|
||||
icon highlight on group layer.
|
||||
|
||||
For SubsetManager
|
||||
|
||||
Args:
|
||||
instance (dict): instance representation from subsetmanager model
|
||||
"""
|
||||
stub = _get_stub()
|
||||
|
||||
if not stub:
|
||||
return
|
||||
|
||||
stub.remove_instance(instance.get("uuid"))
|
||||
layer = stub.get_layer(instance.get("uuid"))
|
||||
if layer:
|
||||
stub.rename_layer(instance.get("uuid"),
|
||||
layer.name.replace(stub.PUBLISH_ICON, ''))
|
||||
|
||||
|
||||
def _get_stub():
|
||||
"""Handle pulling stub from PS to run operations on host
|
||||
|
||||
Returns:
|
||||
(PhotoshopServerStub) or None
|
||||
"""
|
||||
try:
|
||||
stub = lib.stub() # only after Photoshop is up
|
||||
except lib.ConnectionNotEstablishedYet:
|
||||
print("Not connected yet, ignoring")
|
||||
return
|
||||
|
||||
if not stub.get_active_document_name():
|
||||
return
|
||||
|
||||
return stub
|
||||
|
||||
|
||||
def containerise(
|
||||
name, namespace, layer, context, loader=None, suffix="_CON"
|
||||
):
|
||||
"""Imprint layer with metadata
|
||||
|
||||
Containerisation enables a tracking of version, author and origin
|
||||
for loaded assets.
|
||||
|
||||
Arguments:
|
||||
name (str): Name of resulting assembly
|
||||
namespace (str): Namespace under which to host container
|
||||
layer (PSItem): Layer to containerise
|
||||
context (dict): Asset information
|
||||
loader (str, optional): Name of loader used to produce this container.
|
||||
suffix (str, optional): Suffix of container, defaults to `_CON`.
|
||||
|
||||
Returns:
|
||||
container (str): Name of container assembly
|
||||
"""
|
||||
layer.name = name + suffix
|
||||
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": pipeline.AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace,
|
||||
"loader": str(loader),
|
||||
"representation": str(context["representation"]["_id"]),
|
||||
"members": [str(layer.id)]
|
||||
}
|
||||
stub = lib.stub()
|
||||
stub.imprint(layer, data)
|
||||
|
||||
return layer
|
||||
69
openpype/hosts/photoshop/api/plugin.py
Normal file
69
openpype/hosts/photoshop/api/plugin.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import re
|
||||
|
||||
import avalon.api
|
||||
from .launch_logic import stub
|
||||
|
||||
|
||||
def get_unique_layer_name(layers, asset_name, subset_name):
|
||||
"""
|
||||
Gets all layer names and if 'asset_name_subset_name' is present, it
|
||||
increases suffix by 1 (eg. creates unique layer name - for Loader)
|
||||
Args:
|
||||
layers (list) of dict with layers info (name, id etc.)
|
||||
asset_name (string):
|
||||
subset_name (string):
|
||||
|
||||
Returns:
|
||||
(string): name_00X (without version)
|
||||
"""
|
||||
name = "{}_{}".format(asset_name, subset_name)
|
||||
names = {}
|
||||
for layer in layers:
|
||||
layer_name = re.sub(r'_\d{3}$', '', layer.name)
|
||||
if layer_name in names.keys():
|
||||
names[layer_name] = names[layer_name] + 1
|
||||
else:
|
||||
names[layer_name] = 1
|
||||
occurrences = names.get(name, 0)
|
||||
|
||||
return "{}_{:0>3d}".format(name, occurrences + 1)
|
||||
|
||||
|
||||
class PhotoshopLoader(avalon.api.Loader):
|
||||
@staticmethod
|
||||
def get_stub():
|
||||
return stub()
|
||||
|
||||
|
||||
class Creator(avalon.api.Creator):
|
||||
"""Creator plugin to create instances in Photoshop
|
||||
|
||||
A LayerSet is created to support any number of layers in an instance. If
|
||||
the selection is used, these layers will be added to the LayerSet.
|
||||
"""
|
||||
|
||||
def process(self):
|
||||
# Photoshop can have multiple LayerSets with the same name, which does
|
||||
# not work with Avalon.
|
||||
msg = "Instance with name \"{}\" already exists.".format(self.name)
|
||||
stub = lib.stub() # only after Photoshop is up
|
||||
for layer in stub.get_layers():
|
||||
if self.name.lower() == layer.Name.lower():
|
||||
msg = QtWidgets.QMessageBox()
|
||||
msg.setIcon(QtWidgets.QMessageBox.Warning)
|
||||
msg.setText(msg)
|
||||
msg.exec_()
|
||||
return False
|
||||
|
||||
# Store selection because adding a group will change selection.
|
||||
with lib.maintained_selection():
|
||||
|
||||
# Add selection to group.
|
||||
if (self.options or {}).get("useSelection"):
|
||||
group = stub.group_selected_layers(self.name)
|
||||
else:
|
||||
group = stub.create_group(self.name)
|
||||
|
||||
stub.imprint(group, self.data)
|
||||
|
||||
return group
|
||||
51
openpype/hosts/photoshop/api/workio.py
Normal file
51
openpype/hosts/photoshop/api/workio.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""Host API required Work Files tool"""
|
||||
import os
|
||||
|
||||
import avalon.api
|
||||
|
||||
from . import lib
|
||||
|
||||
|
||||
def _active_document():
|
||||
document_name = lib.stub().get_active_document_name()
|
||||
if not document_name:
|
||||
return None
|
||||
|
||||
return document_name
|
||||
|
||||
|
||||
def file_extensions():
|
||||
return avalon.api.HOST_WORKFILE_EXTENSIONS["photoshop"]
|
||||
|
||||
|
||||
def has_unsaved_changes():
|
||||
if _active_document():
|
||||
return not lib.stub().is_saved()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def save_file(filepath):
|
||||
_, ext = os.path.splitext(filepath)
|
||||
lib.stub().saveAs(filepath, ext[1:], True)
|
||||
|
||||
|
||||
def open_file(filepath):
|
||||
lib.stub().open(filepath)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def current_file():
|
||||
try:
|
||||
full_name = lib.stub().get_active_document_full_name()
|
||||
if full_name and full_name != "null":
|
||||
return os.path.normpath(full_name).replace("\\", "/")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def work_root(session):
|
||||
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
|
||||
495
openpype/hosts/photoshop/api/ws_stub.py
Normal file
495
openpype/hosts/photoshop/api/ws_stub.py
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
"""
|
||||
Stub handling connection from server to client.
|
||||
Used anywhere solution is calling client methods.
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import attr
|
||||
from wsrpc_aiohttp import WebSocketAsync
|
||||
|
||||
from avalon.tools.webserver.app import WebServerTool
|
||||
|
||||
|
||||
@attr.s
|
||||
class PSItem(object):
|
||||
"""
|
||||
Object denoting layer or group item in PS. Each item is created in
|
||||
PS by any Loader, but contains same fields, which are being used
|
||||
in later processing.
|
||||
"""
|
||||
# metadata
|
||||
id = attr.ib() # id created by AE, could be used for querying
|
||||
name = attr.ib() # name of item
|
||||
group = attr.ib(default=None) # item type (footage, folder, comp)
|
||||
parents = attr.ib(factory=list)
|
||||
visible = attr.ib(default=True)
|
||||
type = attr.ib(default=None)
|
||||
# all imported elements, single for
|
||||
members = attr.ib(factory=list)
|
||||
long_name = attr.ib(default=None)
|
||||
color_code = attr.ib(default=None) # color code of layer
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
PUBLISH_ICON = '\u2117 '
|
||||
LOADED_ICON = '\u25bc'
|
||||
|
||||
def __init__(self):
|
||||
self.websocketserver = WebServerTool.get_instance()
|
||||
self.client = self.get_client()
|
||||
|
||||
@staticmethod
|
||||
def get_client():
|
||||
"""
|
||||
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
|
||||
|
||||
def open(self, path):
|
||||
"""Open file located at 'path' (local).
|
||||
|
||||
Args:
|
||||
path(string): file path locally
|
||||
Returns: 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.
|
||||
|
||||
Args:
|
||||
layer: (PSItem)
|
||||
layers_meta: full list from Headline (for performance in loops)
|
||||
Returns:
|
||||
"""
|
||||
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
|
||||
|
||||
Stores metadata in format:
|
||||
[{
|
||||
"active":true,
|
||||
"subset":"imageBG",
|
||||
"family":"image",
|
||||
"id":"pyblish.avalon.instance",
|
||||
"asset":"Town",
|
||||
"uuid": "8"
|
||||
}] - for created instances
|
||||
OR
|
||||
[{
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": "pyblish.avalon.instance",
|
||||
"name": "imageMG",
|
||||
"namespace": "Jungle_imageMG_001",
|
||||
"loader": "ImageLoader",
|
||||
"representation": "5fbfc0ee30a946093c6ff18a",
|
||||
"members": [
|
||||
"40"
|
||||
]
|
||||
}] - for loaded instances
|
||||
|
||||
Args:
|
||||
layer (PSItem):
|
||||
data(string): json representation for single layer
|
||||
all_layers (list of PSItem): for performance, could be
|
||||
injected for usage in loop, if not, single call will be
|
||||
triggered
|
||||
layers_meta(string): json representation from Headline
|
||||
(for performance - provide only if imprint is in
|
||||
loop - value should be same)
|
||||
Returns: None
|
||||
"""
|
||||
if not layers_meta:
|
||||
layers_meta = self.get_layers_metadata()
|
||||
|
||||
# json.dumps writes integer values in a dictionary to string, so
|
||||
# anticipating it here.
|
||||
if str(layer.id) in layers_meta and layers_meta[str(layer.id)]:
|
||||
if data:
|
||||
layers_meta[str(layer.id)].update(data)
|
||||
else:
|
||||
layers_meta.pop(str(layer.id))
|
||||
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 layer_id in layers_meta:
|
||||
if int(layer_id) in layer_ids:
|
||||
cleaned_data.append(layers_meta[layer_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.
|
||||
|
||||
Returns: <list of PSItem>
|
||||
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_layer(self, layer_id):
|
||||
"""
|
||||
Returns PSItem for specific 'layer_id' or None if not found
|
||||
Args:
|
||||
layer_id (string): unique layer id, stored in 'uuid' field
|
||||
|
||||
Returns:
|
||||
(PSItem) or None
|
||||
"""
|
||||
layers = self.get_layers()
|
||||
for layer in layers:
|
||||
if str(layer.id) == str(layer_id):
|
||||
return layer
|
||||
|
||||
def get_layers_in_layers(self, layers):
|
||||
"""Return all layers that belong to layers (might be groups).
|
||||
|
||||
Args:
|
||||
layers <list of PSItem>:
|
||||
|
||||
Returns:
|
||||
<list of PSItem>
|
||||
"""
|
||||
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)
|
||||
|
||||
Returns:
|
||||
<PSItem>
|
||||
"""
|
||||
enhanced_name = self.PUBLISH_ICON + name
|
||||
ret = self.websocketserver.call(
|
||||
self.client.call('Photoshop.create_group', name=enhanced_name)
|
||||
)
|
||||
# create group on PS is asynchronous, returns only id
|
||||
return PSItem(id=ret, name=name, group=True)
|
||||
|
||||
def group_selected_layers(self, name):
|
||||
"""Group selected layers into new LayerSet (eg. group)
|
||||
|
||||
Returns:
|
||||
(Layer)
|
||||
"""
|
||||
enhanced_name = self.PUBLISH_ICON + name
|
||||
res = self.websocketserver.call(
|
||||
self.client.call(
|
||||
'Photoshop.group_selected_layers', name=enhanced_name
|
||||
)
|
||||
)
|
||||
res = self._to_records(res)
|
||||
if res:
|
||||
rec = res.pop()
|
||||
rec.name = rec.name.replace(self.PUBLISH_ICON, '')
|
||||
return rec
|
||||
raise ValueError("No group record returned")
|
||||
|
||||
def get_selected_layers(self):
|
||||
"""Get a list of actually selected layers.
|
||||
|
||||
Returns: <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):
|
||||
"""Selects specified layers in Photoshop by its ids.
|
||||
|
||||
Args:
|
||||
layers: <list of Layer('id':XX, 'name':"YYY")>
|
||||
"""
|
||||
layers_id = [str(lay.id) for lay in layers]
|
||||
self.websocketserver.call(
|
||||
self.client.call(
|
||||
'Photoshop.select_layers',
|
||||
layers=json.dumps(layers_id)
|
||||
)
|
||||
)
|
||||
|
||||
def get_active_document_full_name(self):
|
||||
"""Returns full name with path of active document via ws call
|
||||
|
||||
Returns(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
|
||||
|
||||
Returns(string):
|
||||
file name
|
||||
"""
|
||||
return self.websocketserver.call(
|
||||
self.client.call('Photoshop.get_active_document_name')
|
||||
)
|
||||
|
||||
def is_saved(self):
|
||||
"""Returns true if no changes in active document
|
||||
|
||||
Returns:
|
||||
<boolean>
|
||||
"""
|
||||
return self.websocketserver.call(
|
||||
self.client.call('Photoshop.is_saved')
|
||||
)
|
||||
|
||||
def save(self):
|
||||
"""Saves active document"""
|
||||
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
|
||||
|
||||
Args:
|
||||
image_path(string): full local path
|
||||
ext: <string psd|jpg|png>
|
||||
as_copy: <boolean>
|
||||
Returns: 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'
|
||||
|
||||
Args:
|
||||
layer_id: <int>
|
||||
visibility: <true - set visible, false - hide>
|
||||
Returns: 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)
|
||||
|
||||
Returns:
|
||||
(string): - json documents
|
||||
example:
|
||||
{"8":{"active":true,"subset":"imageBG",
|
||||
"family":"image","id":"pyblish.avalon.instance",
|
||||
"asset":"Town"}}
|
||||
8 is layer(group) id - used for deletion, update etc.
|
||||
"""
|
||||
layers_data = {}
|
||||
res = self.websocketserver.call(self.client.call('Photoshop.read'))
|
||||
try:
|
||||
layers_data = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
# format of metadata changed from {} to [] because of standardization
|
||||
# keep current implementation logic as its working
|
||||
if not isinstance(layers_data, dict):
|
||||
temp_layers_meta = {}
|
||||
for layer_meta in layers_data:
|
||||
layer_id = layer_meta.get("uuid")
|
||||
if not layer_id:
|
||||
layer_id = layer_meta.get("members")[0]
|
||||
|
||||
temp_layers_meta[layer_id] = layer_meta
|
||||
layers_data = temp_layers_meta
|
||||
else:
|
||||
# legacy version of metadata
|
||||
for layer_id, layer_meta in layers_data.items():
|
||||
if layer_meta.get("schema") != "openpype:container-2.0":
|
||||
layer_meta["uuid"] = str(layer_id)
|
||||
else:
|
||||
layer_meta["members"] = [str(layer_id)]
|
||||
|
||||
return layers_data
|
||||
|
||||
def import_smart_object(self, path, layer_name, as_reference=False):
|
||||
"""Import the file at `path` as a smart object to active document.
|
||||
|
||||
Args:
|
||||
path (str): File path to import.
|
||||
layer_name (str): Unique layer name to differentiate how many times
|
||||
same smart object was loaded
|
||||
as_reference (bool): pull in content or reference
|
||||
"""
|
||||
enhanced_name = self.LOADED_ICON + layer_name
|
||||
res = self.websocketserver.call(
|
||||
self.client.call(
|
||||
'Photoshop.import_smart_object',
|
||||
path=path,
|
||||
name=enhanced_name,
|
||||
as_reference=as_reference
|
||||
)
|
||||
)
|
||||
rec = self._to_records(res).pop()
|
||||
if rec:
|
||||
rec.name = rec.name.replace(self.LOADED_ICON, '')
|
||||
return rec
|
||||
|
||||
def replace_smart_object(self, layer, path, layer_name):
|
||||
"""Replace the smart object `layer` with file at `path`
|
||||
|
||||
Args:
|
||||
layer (PSItem):
|
||||
path (str): File to import.
|
||||
layer_name (str): Unique layer name to differentiate how many times
|
||||
same smart object was loaded
|
||||
"""
|
||||
enhanced_name = self.LOADED_ICON + layer_name
|
||||
self.websocketserver.call(
|
||||
self.client.call(
|
||||
'Photoshop.replace_smart_object',
|
||||
layer_id=layer.id,
|
||||
path=path,
|
||||
name=enhanced_name
|
||||
)
|
||||
)
|
||||
|
||||
def delete_layer(self, layer_id):
|
||||
"""Deletes specific layer by it's id.
|
||||
|
||||
Args:
|
||||
layer_id (int): id of layer to delete
|
||||
"""
|
||||
self.websocketserver.call(
|
||||
self.client.call('Photoshop.delete_layer', layer_id=layer_id)
|
||||
)
|
||||
|
||||
def rename_layer(self, layer_id, name):
|
||||
"""Renames specific layer by it's id.
|
||||
|
||||
Args:
|
||||
layer_id (int): id of layer to delete
|
||||
name (str): new name
|
||||
"""
|
||||
self.websocketserver.call(
|
||||
self.client.call(
|
||||
'Photoshop.rename_layer',
|
||||
layer_id=layer_id,
|
||||
name=name
|
||||
)
|
||||
)
|
||||
|
||||
def remove_instance(self, instance_id):
|
||||
cleaned_data = {}
|
||||
|
||||
for key, instance in self.get_layers_metadata().items():
|
||||
if key != instance_id:
|
||||
cleaned_data[key] = instance
|
||||
|
||||
payload = json.dumps(cleaned_data, indent=4)
|
||||
|
||||
self.websocketserver.call(
|
||||
self.client.call('Photoshop.imprint', payload=payload)
|
||||
)
|
||||
|
||||
def get_extension_version(self):
|
||||
"""Returns version number of installed extension."""
|
||||
return self.websocketserver.call(
|
||||
self.client.call('Photoshop.get_extension_version')
|
||||
)
|
||||
|
||||
def close(self):
|
||||
"""Shutting down PS and process too.
|
||||
|
||||
For webpublishing only.
|
||||
"""
|
||||
# TODO change client.call to method with checks for client
|
||||
self.websocketserver.call(self.client.call('Photoshop.close'))
|
||||
|
||||
def _to_records(self, res):
|
||||
"""Converts string json representation into list of PSItem for
|
||||
dot notation access to work.
|
||||
|
||||
Args:
|
||||
res (string): valid json
|
||||
|
||||
Returns:
|
||||
<list of PSItem>
|
||||
"""
|
||||
try:
|
||||
layers_data = json.loads(res)
|
||||
except json.decoder.JSONDecodeError:
|
||||
raise ValueError("Received broken JSON {}".format(res))
|
||||
ret = []
|
||||
|
||||
# convert to AEItem to use dot donation
|
||||
if isinstance(layers_data, dict):
|
||||
layers_data = [layers_data]
|
||||
for d in layers_data:
|
||||
# currently implemented and expected fields
|
||||
ret.append(PSItem(
|
||||
d.get('id'),
|
||||
d.get('name'),
|
||||
d.get('group'),
|
||||
d.get('parents'),
|
||||
d.get('visible'),
|
||||
d.get('type'),
|
||||
d.get('members'),
|
||||
d.get('long_name'),
|
||||
d.get("color_code")
|
||||
))
|
||||
return ret
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from Qt import QtWidgets
|
||||
import openpype.api
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class CreateImage(openpype.api.Creator):
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import re
|
||||
|
||||
|
||||
def get_unique_layer_name(layers, asset_name, subset_name):
|
||||
"""
|
||||
Gets all layer names and if 'asset_name_subset_name' is present, it
|
||||
increases suffix by 1 (eg. creates unique layer name - for Loader)
|
||||
Args:
|
||||
layers (list) of dict with layers info (name, id etc.)
|
||||
asset_name (string):
|
||||
subset_name (string):
|
||||
|
||||
Returns:
|
||||
(string): name_00X (without version)
|
||||
"""
|
||||
name = "{}_{}".format(asset_name, subset_name)
|
||||
names = {}
|
||||
for layer in layers:
|
||||
layer_name = re.sub(r'_\d{3}$', '', layer.name)
|
||||
if layer_name in names.keys():
|
||||
names[layer_name] = names[layer_name] + 1
|
||||
else:
|
||||
names[layer_name] = 1
|
||||
occurrences = names.get(name, 0)
|
||||
|
||||
return "{}_{:0>3d}".format(name, occurrences + 1)
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import re
|
||||
|
||||
from avalon import api, photoshop
|
||||
from avalon import api
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
from openpype.hosts.photoshop.api import get_unique_layer_name
|
||||
|
||||
from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
|
||||
|
||||
stub = photoshop.stub()
|
||||
|
||||
class ImageLoader(api.Loader):
|
||||
class ImageLoader(photoshop.PhotoshopLoader):
|
||||
"""Load images
|
||||
|
||||
Stores the imported asset in a container named after the asset.
|
||||
|
|
@ -16,11 +15,14 @@ class ImageLoader(api.Loader):
|
|||
representations = ["*"]
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
layer_name = get_unique_layer_name(stub.get_layers(),
|
||||
context["asset"]["name"],
|
||||
name)
|
||||
stub = self.get_stub()
|
||||
layer_name = get_unique_layer_name(
|
||||
stub.get_layers(),
|
||||
context["asset"]["name"],
|
||||
name
|
||||
)
|
||||
with photoshop.maintained_selection():
|
||||
layer = self.import_layer(self.fname, layer_name)
|
||||
layer = self.import_layer(self.fname, layer_name, stub)
|
||||
|
||||
self[:] = [layer]
|
||||
namespace = namespace or layer_name
|
||||
|
|
@ -35,6 +37,8 @@ class ImageLoader(api.Loader):
|
|||
|
||||
def update(self, container, representation):
|
||||
""" Switch asset or change version """
|
||||
stub = self.get_stub()
|
||||
|
||||
layer = container.pop("layer")
|
||||
|
||||
context = representation.get("context", {})
|
||||
|
|
@ -44,9 +48,9 @@ class ImageLoader(api.Loader):
|
|||
layer_name = "{}_{}".format(context["asset"], context["subset"])
|
||||
# switching assets
|
||||
if namespace_from_container != layer_name:
|
||||
layer_name = get_unique_layer_name(stub.get_layers(),
|
||||
context["asset"],
|
||||
context["subset"])
|
||||
layer_name = get_unique_layer_name(
|
||||
stub.get_layers(), context["asset"], context["subset"]
|
||||
)
|
||||
else: # switching version - keep same name
|
||||
layer_name = container["namespace"]
|
||||
|
||||
|
|
@ -66,6 +70,8 @@ class ImageLoader(api.Loader):
|
|||
Args:
|
||||
container (dict): container to be removed - used to get layer_id
|
||||
"""
|
||||
stub = self.get_stub()
|
||||
|
||||
layer = container.pop("layer")
|
||||
stub.imprint(layer, {})
|
||||
stub.delete_layer(layer.id)
|
||||
|
|
@ -73,5 +79,5 @@ class ImageLoader(api.Loader):
|
|||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
def import_layer(self, file_name, layer_name):
|
||||
def import_layer(self, file_name, layer_name, stub):
|
||||
return stub.import_smart_object(file_name, layer_name)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
import os
|
||||
|
||||
from avalon import api
|
||||
from avalon import photoshop
|
||||
from avalon.pipeline import get_representation_path_from_context
|
||||
from avalon.vendor import qargparse
|
||||
|
||||
from openpype.lib import Anatomy
|
||||
from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
|
||||
|
||||
stub = photoshop.stub()
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
from openpype.hosts.photoshop.api import get_unique_layer_name
|
||||
|
||||
|
||||
class ImageFromSequenceLoader(api.Loader):
|
||||
class ImageFromSequenceLoader(photoshop.PhotoshopLoader):
|
||||
""" Load specifing image from sequence
|
||||
|
||||
Used only as quick load of reference file from a sequence.
|
||||
|
|
@ -35,15 +31,16 @@ class ImageFromSequenceLoader(api.Loader):
|
|||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
if data.get("frame"):
|
||||
self.fname = os.path.join(os.path.dirname(self.fname),
|
||||
data["frame"])
|
||||
self.fname = os.path.join(
|
||||
os.path.dirname(self.fname), data["frame"]
|
||||
)
|
||||
if not os.path.exists(self.fname):
|
||||
return
|
||||
|
||||
stub = photoshop.stub()
|
||||
layer_name = get_unique_layer_name(stub.get_layers(),
|
||||
context["asset"]["name"],
|
||||
name)
|
||||
stub = self.get_stub()
|
||||
layer_name = get_unique_layer_name(
|
||||
stub.get_layers(), context["asset"]["name"], name
|
||||
)
|
||||
|
||||
with photoshop.maintained_selection():
|
||||
layer = stub.import_smart_object(self.fname, layer_name)
|
||||
|
|
@ -95,4 +92,3 @@ class ImageFromSequenceLoader(api.Loader):
|
|||
def remove(self, container):
|
||||
"""No update possible, not containerized."""
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
import re
|
||||
|
||||
from avalon import api, photoshop
|
||||
from avalon import api
|
||||
|
||||
from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
|
||||
|
||||
stub = photoshop.stub()
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
from openpype.hosts.photoshop.api import get_unique_layer_name
|
||||
|
||||
|
||||
class ReferenceLoader(api.Loader):
|
||||
class ReferenceLoader(photoshop.PhotoshopLoader):
|
||||
"""Load reference images
|
||||
|
||||
Stores the imported asset in a container named after the asset.
|
||||
Stores the imported asset in a container named after the asset.
|
||||
|
||||
Inheriting from 'load_image' didn't work because of
|
||||
"Cannot write to closing transport", possible refactor.
|
||||
Inheriting from 'load_image' didn't work because of
|
||||
"Cannot write to closing transport", possible refactor.
|
||||
"""
|
||||
|
||||
families = ["image", "render"]
|
||||
representations = ["*"]
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
layer_name = get_unique_layer_name(stub.get_layers(),
|
||||
context["asset"]["name"],
|
||||
name)
|
||||
stub = self.get_stub()
|
||||
layer_name = get_unique_layer_name(
|
||||
stub.get_layers(), context["asset"]["name"], name
|
||||
)
|
||||
with photoshop.maintained_selection():
|
||||
layer = self.import_layer(self.fname, layer_name)
|
||||
layer = self.import_layer(self.fname, layer_name, stub)
|
||||
|
||||
self[:] = [layer]
|
||||
namespace = namespace or layer_name
|
||||
|
|
@ -39,6 +39,7 @@ class ReferenceLoader(api.Loader):
|
|||
|
||||
def update(self, container, representation):
|
||||
""" Switch asset or change version """
|
||||
stub = self.get_stub()
|
||||
layer = container.pop("layer")
|
||||
|
||||
context = representation.get("context", {})
|
||||
|
|
@ -48,9 +49,9 @@ class ReferenceLoader(api.Loader):
|
|||
layer_name = "{}_{}".format(context["asset"], context["subset"])
|
||||
# switching assets
|
||||
if namespace_from_container != layer_name:
|
||||
layer_name = get_unique_layer_name(stub.get_layers(),
|
||||
context["asset"],
|
||||
context["subset"])
|
||||
layer_name = get_unique_layer_name(
|
||||
stub.get_layers(), context["asset"], context["subset"]
|
||||
)
|
||||
else: # switching version - keep same name
|
||||
layer_name = container["namespace"]
|
||||
|
||||
|
|
@ -65,11 +66,12 @@ class ReferenceLoader(api.Loader):
|
|||
)
|
||||
|
||||
def remove(self, container):
|
||||
"""
|
||||
Removes element from scene: deletes layer + removes from Headline
|
||||
"""Removes element from scene: deletes layer + removes from Headline
|
||||
|
||||
Args:
|
||||
container (dict): container to be removed - used to get layer_id
|
||||
"""
|
||||
stub = self.get_stub()
|
||||
layer = container.pop("layer")
|
||||
stub.imprint(layer, {})
|
||||
stub.delete_layer(layer.id)
|
||||
|
|
@ -77,6 +79,7 @@ class ReferenceLoader(api.Loader):
|
|||
def switch(self, container, representation):
|
||||
self.update(container, representation)
|
||||
|
||||
def import_layer(self, file_name, layer_name):
|
||||
return stub.import_smart_object(file_name, layer_name,
|
||||
as_reference=True)
|
||||
def import_layer(self, file_name, layer_name, stub):
|
||||
return stub.import_smart_object(
|
||||
file_name, layer_name, as_reference=True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import os
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class ClosePS(pyblish.api.ContextPlugin):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
|
||||
import pyblish.api
|
||||
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class CollectCurrentFile(pyblish.api.ContextPlugin):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
import re
|
||||
import pyblish.api
|
||||
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class CollectExtensionVersion(pyblish.api.ContextPlugin):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import pyblish.api
|
||||
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import pyblish.api
|
||||
import os
|
||||
import re
|
||||
|
||||
from avalon import photoshop
|
||||
import pyblish.api
|
||||
|
||||
from openpype.lib import prepare_template_data
|
||||
from openpype.lib.plugin_tools import parse_json
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class CollectRemoteInstances(pyblish.api.ContextPlugin):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import pyblish.api
|
||||
import os
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectWorkfile(pyblish.api.ContextPlugin):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
|
||||
import openpype.api
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class ExtractImage(openpype.api.Extractor):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import os
|
|||
|
||||
import openpype.api
|
||||
import openpype.lib
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class ExtractReview(openpype.api.Extractor):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import openpype.api
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class ExtractSaveScene(openpype.api.Extractor):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import pyblish.api
|
|||
from openpype.action import get_errored_plugins_from_data
|
||||
from openpype.lib import version_up
|
||||
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class IncrementWorkfile(pyblish.api.InstancePlugin):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from avalon import api
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class ValidateInstanceAssetRepair(pyblish.api.Action):
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import re
|
|||
|
||||
import pyblish.api
|
||||
import openpype.api
|
||||
from avalon import photoshop
|
||||
from openpype.hosts.photoshop import api as photoshop
|
||||
|
||||
|
||||
class ValidateNamingRepair(pyblish.api.Action):
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ def main(argv):
|
|||
|
||||
host_name = os.environ["AVALON_APP"].lower()
|
||||
if host_name == "photoshop":
|
||||
from avalon.photoshop.lib import main
|
||||
from openpype.hosts.photoshop.api.lib import main
|
||||
elif host_name == "aftereffects":
|
||||
from avalon.aftereffects.lib import main
|
||||
elif host_name == "harmony":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue