[Automated] Merged develop into main

This commit is contained in:
pypebot 2022-01-12 04:37:50 +01:00 committed by GitHub
commit 3a5cf9eaef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 5758 additions and 264 deletions

View file

@ -3,7 +3,7 @@ import subprocess
from openpype.lib import (
PreLaunchHook,
get_pype_execute_args
get_openpype_execute_args
)
from openpype import PACKAGE_DIR as OPENPYPE_DIR
@ -35,7 +35,7 @@ class NonPythonHostHook(PreLaunchHook):
"non_python_host_launch.py"
)
new_launch_args = get_pype_execute_args(
new_launch_args = get_openpype_execute_args(
"run", script_path, executable_path
)
# Add workfile path if exists
@ -48,4 +48,3 @@ class NonPythonHostHook(PreLaunchHook):
if remainders:
self.launch_context.launch_args.extend(remainders)

View file

@ -1,6 +1,6 @@
from avalon import api
from avalon.houdini import pipeline, lib
from avalon.houdini import pipeline
class AbcLoader(api.Loader):
@ -25,16 +25,9 @@ class AbcLoader(api.Loader):
# Get the root node
obj = hou.node("/obj")
# Create a unique name
counter = 1
# Define node name
namespace = namespace if namespace else context["asset"]["name"]
formatted = "{}_{}".format(namespace, name) if namespace else name
node_name = "{0}_{1:03d}".format(formatted, counter)
children = lib.children_as_string(hou.node("/obj"))
while node_name in children:
counter += 1
node_name = "{0}_{1:03d}".format(formatted, counter)
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a new geo node
container = obj.createNode("geo", node_name=node_name)

View file

@ -1,5 +1,5 @@
from avalon import api
from avalon.houdini import pipeline, lib
from avalon.houdini import pipeline
ARCHIVE_EXPRESSION = ('__import__("_alembic_hom_extensions")'
@ -97,18 +97,9 @@ class CameraLoader(api.Loader):
# Get the root node
obj = hou.node("/obj")
# Create a unique name
counter = 1
asset_name = context["asset"]["name"]
namespace = namespace or asset_name
formatted = "{}_{}".format(namespace, name) if namespace else name
node_name = "{0}_{1:03d}".format(formatted, counter)
children = lib.children_as_string(hou.node("/obj"))
while node_name in children:
counter += 1
node_name = "{0}_{1:03d}".format(formatted, counter)
# Define node name
namespace = namespace if namespace else context["asset"]["name"]
node_name = "{}_{}".format(namespace, name) if namespace else name
# Create a archive node
container = self.create_and_connect(obj, "alembicarchive", node_name)

View file

@ -745,6 +745,33 @@ def namespaced(namespace, new=True):
cmds.namespace(set=original)
@contextlib.contextmanager
def maintained_selection_api():
"""Maintain selection using the Maya Python API.
Warning: This is *not* added to the undo stack.
"""
original = om.MGlobal.getActiveSelectionList()
try:
yield
finally:
om.MGlobal.setActiveSelectionList(original)
@contextlib.contextmanager
def tool(context):
"""Set a tool context during the context manager.
"""
original = cmds.currentCtx()
try:
cmds.setToolTo(context)
yield
finally:
cmds.setToolTo(original)
def polyConstraint(components, *args, **kwargs):
"""Return the list of *components* with the constraints applied.
@ -763,17 +790,25 @@ def polyConstraint(components, *args, **kwargs):
kwargs.pop('mode', None)
with no_undo(flush=False):
with maya.maintained_selection():
# Apply constraint using mode=2 (current and next) so
# it applies to the selection made before it; because just
# a `maya.cmds.select()` call will not trigger the constraint.
with reset_polySelectConstraint():
cmds.select(components, r=1, noExpand=True)
cmds.polySelectConstraint(*args, mode=2, **kwargs)
result = cmds.ls(selection=True)
cmds.select(clear=True)
return result
# Reverting selection to the original selection using
# `maya.cmds.select` can be slow in rare cases where previously
# `maya.cmds.polySelectConstraint` had set constrain to "All and Next"
# and the "Random" setting was activated. To work around this we
# revert to the original selection using the Maya API. This is safe
# since we're not generating any undo change anyway.
with tool("selectSuperContext"):
# Selection can be very slow when in a manipulator mode.
# So we force the selection context which is fast.
with maintained_selection_api():
# Apply constraint using mode=2 (current and next) so
# it applies to the selection made before it; because just
# a `maya.cmds.select()` call will not trigger the constraint.
with reset_polySelectConstraint():
cmds.select(components, r=1, noExpand=True)
cmds.polySelectConstraint(*args, mode=2, **kwargs)
result = cmds.ls(selection=True)
cmds.select(clear=True)
return result
@contextlib.contextmanager

View file

@ -8,6 +8,8 @@ from collections import defaultdict
from openpype.widgets.message_window import ScrollMessageBox
from Qt import QtWidgets
from openpype.hosts.maya.api.plugin import get_reference_node
class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
"""Specific loader for lookdev"""
@ -70,7 +72,7 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader):
# Get reference node from container members
members = cmds.sets(node, query=True, nodesOnly=True)
reference_node = self._get_reference_node(members)
reference_node = get_reference_node(members, log=self.log)
shader_nodes = cmds.ls(members, type='shadingEngine')
orig_nodes = set(self._get_nodes_with_shader(shader_nodes))

View 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:
![Avalon Panel](panel.PNG "Avalon Panel")
## 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

View file

@ -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",
]

Binary file not shown.

View 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>

View 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>

File diff suppressed because it is too large Load diff

View 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");

View 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});

View 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

File diff suppressed because one or more lines are too long

View 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();
})();

File diff suppressed because one or more lines are too long

View 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 "&nbsp;"),
// 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");
};
}
}());

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View 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>

View 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"

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View 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

View 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

View 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("\\", "/")

View 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

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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
)

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -1,6 +1,6 @@
import pyblish.api
from avalon import photoshop
from openpype.hosts.photoshop import api as photoshop
class CollectInstances(pyblish.api.ContextPlugin):

View file

@ -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):

View file

@ -1,5 +1,5 @@
import pyblish.api
import os
import pyblish.api
class CollectWorkfile(pyblish.api.ContextPlugin):

View file

@ -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):

View file

@ -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):

View file

@ -1,5 +1,5 @@
import openpype.api
from avalon import photoshop
from openpype.hosts.photoshop import api as photoshop
class ExtractSaveScene(openpype.api.Extractor):

View file

@ -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):

View file

@ -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):

View file

@ -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):

View file

@ -4,7 +4,7 @@ import shutil
from openpype.hosts import tvpaint
from openpype.lib import (
PreLaunchHook,
get_pype_execute_args
get_openpype_execute_args
)
import avalon
@ -30,7 +30,7 @@ class TvpaintPrelaunchHook(PreLaunchHook):
while self.launch_context.launch_args:
remainders.append(self.launch_context.launch_args.pop(0))
new_launch_args = get_pype_execute_args(
new_launch_args = get_openpype_execute_args(
"run", self.launch_script_path(), executable_path
)

View file

@ -24,10 +24,13 @@ from .env_tools import (
from .terminal import Terminal
from .execute import (
get_openpype_execute_args,
get_pype_execute_args,
get_linux_launcher_args,
execute,
run_subprocess,
run_openpype_process,
clean_envs_for_openpype_process,
path_to_subprocess_arg,
CREATE_NO_WINDOW
)
@ -173,10 +176,13 @@ from .pype_info import (
terminal = Terminal
__all__ = [
"get_openpype_execute_args",
"get_pype_execute_args",
"get_linux_launcher_args",
"execute",
"run_subprocess",
"run_openpype_process",
"clean_envs_for_openpype_process",
"path_to_subprocess_arg",
"CREATE_NO_WINDOW",

View file

@ -138,6 +138,49 @@ def run_subprocess(*args, **kwargs):
return full_output
def clean_envs_for_openpype_process(env=None):
"""Modify environemnts that may affect OpenPype process.
Main reason to implement this function is to pop PYTHONPATH which may be
affected by in-host environments.
"""
if env is None:
env = os.environ
return {
key: value
for key, value in env.items()
if key not in ("PYTHONPATH",)
}
def run_openpype_process(*args, **kwargs):
"""Execute OpenPype process with passed arguments and wait.
Wrapper for 'run_process' which prepends OpenPype executable arguments
before passed arguments and define environments if are not passed.
Values from 'os.environ' are used for environments if are not passed.
They are cleaned using 'clean_envs_for_openpype_process' function.
Example:
```
run_openpype_process("run", "<path to .py script>")
```
Args:
*args (tuple): OpenPype cli arguments.
**kwargs (dict): Keyword arguments for for subprocess.Popen.
"""
args = get_openpype_execute_args(*args)
env = kwargs.pop("env", None)
# Keep env untouched if are passed and not empty
if not env:
# Skip envs that can affect OpenPype process
# - fill more if you find more
env = clean_envs_for_openpype_process(os.environ)
return run_subprocess(args, env=env, **kwargs)
def path_to_subprocess_arg(path):
"""Prepare path for subprocess arguments.
@ -147,6 +190,18 @@ def path_to_subprocess_arg(path):
def get_pype_execute_args(*args):
"""Backwards compatible function for 'get_openpype_execute_args'."""
import traceback
log = Logger.get_logger("get_pype_execute_args")
stack = "\n".join(traceback.format_stack())
log.warning((
"Using deprecated function 'get_pype_execute_args'. Called from:\n{}"
).format(stack))
return get_openpype_execute_args(*args)
def get_openpype_execute_args(*args):
"""Arguments to run pype command.
Arguments for subprocess when need to spawn new pype process. Which may be

View file

@ -7,7 +7,7 @@ import socket
import openpype.version
from openpype.settings.lib import get_local_settings
from .execute import get_pype_execute_args
from .execute import get_openpype_execute_args
from .local_settings import get_local_site_id
from .python_module_tools import import_filepath
@ -71,7 +71,7 @@ def is_running_staging():
def get_pype_info():
"""Information about currently used Pype process."""
executable_args = get_pype_execute_args()
executable_args = get_openpype_execute_args()
if is_running_from_build():
version_type = "build"
else:

View file

@ -42,6 +42,7 @@ DEFAULT_OPENPYPE_MODULES = (
"settings_action",
"standalonepublish_action",
"job_queue",
"timers_manager",
)

View file

@ -14,7 +14,7 @@ import uuid
import ftrack_api
import pymongo
from openpype.lib import (
get_pype_execute_args,
get_openpype_execute_args,
OpenPypeMongoConnection,
get_openpype_version,
get_build_version,
@ -136,7 +136,7 @@ def legacy_server(ftrack_url):
if subproc is None:
if subproc_failed_count < max_fail_count:
args = get_pype_execute_args("run", subproc_path)
args = get_openpype_execute_args("run", subproc_path)
subproc = subprocess.Popen(
args,
stdout=subprocess.PIPE
@ -248,7 +248,7 @@ def main_loop(ftrack_url):
["Username", getpass.getuser()],
["Host Name", host_name],
["Host IP", socket.gethostbyname(host_name)],
["OpenPype executable", get_pype_execute_args()[-1]],
["OpenPype executable", get_openpype_execute_args()[-1]],
["OpenPype version", get_openpype_version() or "N/A"],
["OpenPype build version", get_build_version() or "N/A"]
]

View file

@ -6,7 +6,7 @@ import threading
import traceback
import subprocess
from openpype.api import Logger
from openpype.lib import get_pype_execute_args
from openpype.lib import get_openpype_execute_args
class SocketThread(threading.Thread):
@ -59,7 +59,7 @@ class SocketThread(threading.Thread):
env = os.environ.copy()
env["OPENPYPE_PROCESS_MONGO_ID"] = str(Logger.mongo_process_id)
# OpenPype executable (with path to start script if not build)
args = get_pype_execute_args(
args = get_openpype_execute_args(
# Add `run` command
"run",
self.filepath,

View file

@ -1,7 +1,7 @@
import os
import platform
import subprocess
from openpype.lib import get_pype_execute_args
from openpype.lib import get_openpype_execute_args
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayAction
@ -35,7 +35,7 @@ class StandAlonePublishAction(OpenPypeModule, ITrayAction):
self.publish_paths.extend(publish_paths)
def run_standalone_publisher(self):
args = get_pype_execute_args("standalonepublisher")
args = get_openpype_execute_args("standalonepublisher")
kwargs = {}
if platform.system().lower() == "darwin":
new_args = ["open", "-na", args.pop(0), "--args"]

View file

@ -13,7 +13,7 @@ import pyblish
import openpype
import openpype.api
from openpype.lib import (
get_pype_execute_args,
run_openpype_process,
get_transcode_temp_directory,
convert_for_ffmpeg,
@ -168,9 +168,8 @@ class ExtractBurnin(openpype.api.Extractor):
anatomy = instance.context.data["anatomy"]
scriptpath = self.burnin_script_path()
# Executable args that will execute the script
# [pype executable, *pype script, "run"]
executable_args = get_pype_execute_args("run", scriptpath)
# Args that will execute the script
executable_args = ["run", scriptpath]
burnins_per_repres = self._get_burnins_per_representations(
instance, burnin_defs
)
@ -313,7 +312,7 @@ class ExtractBurnin(openpype.api.Extractor):
if platform.system().lower() == "windows":
process_kwargs["creationflags"] = CREATE_NO_WINDOW
openpype.api.run_subprocess(args, **process_kwargs)
run_openpype_process(*args, **process_kwargs)
# Remove the temporary json
os.remove(temporary_json_filepath)

View file

@ -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":

View file

@ -51,6 +51,9 @@
"border-hover": "rgba(168, 175, 189, .3)",
"border-focus": "rgb(92, 173, 214)",
"delete-btn-bg": "rgb(201, 54, 54)",
"delete-btn-bg-disabled": "rgba(201, 54, 54, 64)",
"tab-widget": {
"bg": "#21252B",
"bg-selected": "#434a56",

View file

@ -734,6 +734,13 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: {color:bg-view-hover};
}
#DeleteButton {
background: {color:delete-btn-bg};
}
#DeleteButton:disabled {
background: {color:delete-btn-bg-disabled};
}
/* Launcher specific stylesheets */
#IconView[mode="icon"] {
/* font size can't be set on items */

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -1,6 +1,7 @@
import os
from Qt import QtCore, QtGui
from openpype.style import get_objected_colors
from avalon.vendor import qtawesome
@ -90,6 +91,17 @@ class ResourceCache:
icon.addPixmap(disabled_pix, QtGui.QIcon.Disabled, QtGui.QIcon.Off)
return icon
@classmethod
def get_warning_pixmap(cls):
src_image = get_warning_image()
colors = get_objected_colors()
color_value = colors["delete-btn-bg"]
return paint_image_with_color(
src_image,
color_value.get_qcolor()
)
def get_remove_image():
image_path = os.path.join(
@ -100,6 +112,15 @@ def get_remove_image():
return QtGui.QImage(image_path)
def get_warning_image():
image_path = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"images",
"warning.png"
)
return QtGui.QImage(image_path)
def paint_image_with_color(image, color):
"""TODO: This function should be imported from utils.

View file

@ -4,6 +4,7 @@ from .constants import (
NAME_ALLOWED_SYMBOLS,
NAME_REGEX
)
from .style import ResourceCache
from openpype.lib import (
create_project,
PROJECT_NAME_ALLOWED_SYMBOLS,
@ -13,7 +14,7 @@ from openpype.style import load_stylesheet
from openpype.tools.utils import PlaceholderLineEdit
from avalon.api import AvalonMongoDB
from Qt import QtWidgets, QtCore
from Qt import QtWidgets, QtCore, QtGui
class NameTextEdit(QtWidgets.QLineEdit):
@ -291,42 +292,41 @@ class CreateProjectDialog(QtWidgets.QDialog):
return project_names, project_codes
class _SameSizeBtns(QtWidgets.QPushButton):
"""Button that keep width of all button added as related.
# TODO PixmapLabel should be moved to 'utils' in other future PR so should be
# imported from there
class PixmapLabel(QtWidgets.QLabel):
"""Label resizing image to height of font."""
def __init__(self, pixmap, parent):
super(PixmapLabel, self).__init__(parent)
self._empty_pixmap = QtGui.QPixmap(0, 0)
self._source_pixmap = pixmap
This happens without changing min/max/fix size of button. Which is
welcomed for multidisplay desktops with different resolution.
"""
def __init__(self, *args, **kwargs):
super(_SameSizeBtns, self).__init__(*args, **kwargs)
self._related_btns = []
def set_source_pixmap(self, pixmap):
"""Change source image."""
self._source_pixmap = pixmap
self._set_resized_pix()
def add_related_btn(self, btn):
"""Add related button which should be checked for width.
def _get_pix_size(self):
size = self.fontMetrics().height() * 4
return size, size
Args:
btn (_SameSizeBtns): Other object of _SameSizeBtns.
"""
self._related_btns.append(btn)
def _set_resized_pix(self):
if self._source_pixmap is None:
self.setPixmap(self._empty_pixmap)
return
width, height = self._get_pix_size()
self.setPixmap(
self._source_pixmap.scaled(
width,
height,
QtCore.Qt.KeepAspectRatio,
QtCore.Qt.SmoothTransformation
)
)
def hint_width(self):
"""Get size hint of button not related to others."""
return super(_SameSizeBtns, self).sizeHint().width()
def sizeHint(self):
"""Calculate size hint based on size hint of this button and related.
If width is lower than any other button it is changed to higher.
"""
result = super(_SameSizeBtns, self).sizeHint()
width = result.width()
for btn in self._related_btns:
btn_width = btn.hint_width()
if btn_width > width:
width = btn_width
result.setWidth(width)
return result
def resizeEvent(self, event):
self._set_resized_pix()
super(PixmapLabel, self).resizeEvent(event)
class ConfirmProjectDeletion(QtWidgets.QDialog):
@ -336,35 +336,50 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self.setWindowTitle("Delete project?")
message = (
"Project <b>\"{}\"</b> with all related data will be"
" permanently removed from the database (This actions won't remove"
" any files on disk)."
).format(project_name)
message_label = QtWidgets.QLabel(message, self)
top_widget = QtWidgets.QWidget(self)
warning_pixmap = ResourceCache.get_warning_pixmap()
warning_icon_label = PixmapLabel(warning_pixmap, top_widget)
message_label = QtWidgets.QLabel(top_widget)
message_label.setWordWrap(True)
message_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
message_label.setText((
"<b>WARNING: This cannot be undone.</b><br/><br/>"
"Project <b>\"{}\"</b> with all related data will be"
" permanently removed from the database. (This action won't remove"
" any files on disk.)"
).format(project_name))
top_layout = QtWidgets.QHBoxLayout(top_widget)
top_layout.setContentsMargins(0, 0, 0, 0)
top_layout.addWidget(
warning_icon_label, 0,
QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter
)
top_layout.addWidget(message_label, 1)
question_label = QtWidgets.QLabel("<b>Are you sure?</b>", self)
confirm_input = PlaceholderLineEdit(self)
confirm_input.setPlaceholderText("Type \"Delete\" to confirm...")
confirm_input.setPlaceholderText(
"Type \"{}\" to confirm...".format(project_name)
)
cancel_btn = _SameSizeBtns("Cancel", self)
cancel_btn = QtWidgets.QPushButton("Cancel", self)
cancel_btn.setToolTip("Cancel deletion of the project")
confirm_btn = _SameSizeBtns("Delete", self)
confirm_btn = QtWidgets.QPushButton("Permanently Delete Project", self)
confirm_btn.setObjectName("DeleteButton")
confirm_btn.setEnabled(False)
confirm_btn.setToolTip("Confirm deletion")
cancel_btn.add_related_btn(confirm_btn)
confirm_btn.add_related_btn(cancel_btn)
btns_layout = QtWidgets.QHBoxLayout()
btns_layout.addStretch(1)
btns_layout.addWidget(cancel_btn, 0)
btns_layout.addWidget(confirm_btn, 0)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(message_label, 0)
layout.addWidget(top_widget, 0)
layout.addStretch(1)
layout.addWidget(question_label, 0)
layout.addWidget(confirm_input, 0)
@ -379,6 +394,7 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self._confirm_btn = confirm_btn
self._confirm_input = confirm_input
self._result = 0
self._project_name = project_name
self.setMinimumWidth(480)
self.setMaximumWidth(650)
@ -411,5 +427,5 @@ class ConfirmProjectDeletion(QtWidgets.QDialog):
self._on_confirm_click()
def _on_confirm_text_change(self):
enabled = self._confirm_input.text().lower() == "delete"
enabled = self._confirm_input.text() == self._project_name
self._confirm_btn.setEnabled(enabled)

View file

@ -78,7 +78,9 @@ class ProjectManagerWindow(QtWidgets.QWidget):
)
create_folders_btn.setEnabled(False)
remove_projects_btn = QtWidgets.QPushButton(project_widget)
remove_projects_btn = QtWidgets.QPushButton(
"Delete project", project_widget
)
remove_projects_btn.setIcon(ResourceCache.get_icon("remove"))
remove_projects_btn.setObjectName("IconBtn")

View file

@ -10,7 +10,7 @@ from .constants import HOST_NAME
from avalon import io
from openpype.api import execute, Logger
from openpype.lib import (
get_pype_execute_args,
get_openpype_execute_args,
apply_project_environments_value
)
@ -193,7 +193,7 @@ def cli_publish(data, publish_paths, gui=True):
project_name = os.environ["AVALON_PROJECT"]
env_copy = apply_project_environments_value(project_name, envcopy)
args = get_pype_execute_args("run", PUBLISH_SCRIPT_PATH)
args = get_openpype_execute_args("run", PUBLISH_SCRIPT_PATH)
result = execute(args, env=envcopy)
result = {}

View file

@ -14,7 +14,7 @@ from openpype.api import (
resources,
get_system_settings
)
from openpype.lib import get_pype_execute_args
from openpype.lib import get_openpype_execute_args
from openpype.modules import TrayModulesManager
from openpype import style
from openpype.settings import (
@ -208,10 +208,10 @@ class TrayManager:
First creates new process with same argument and close current tray.
"""
args = get_pype_execute_args()
args = get_openpype_execute_args()
# Create a copy of sys.argv
additional_args = list(sys.argv)
# Check last argument from `get_pype_execute_args`
# Check last argument from `get_openpype_execute_args`
# - when running from code it is the same as first from sys.argv
if args[-1] == additional_args[0]:
additional_args.pop(0)