From e94aa5311651c3219c5afe8219b0c5ecaa35e4c7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:00:45 +0100 Subject: [PATCH 01/13] moved photoshop from avalon to openpype --- openpype/hosts/photoshop/api/README.md | 255 ++++ openpype/hosts/photoshop/api/__init__.py | 134 +- openpype/hosts/photoshop/api/extension.zxp | Bin 0 -> 54111 bytes openpype/hosts/photoshop/api/extension/.debug | 9 + .../photoshop/api/extension/CSXS/manifest.xml | 53 + .../api/extension/client/CSInterface.js | 1193 +++++++++++++++++ .../photoshop/api/extension/client/client.js | 300 +++++ .../api/extension/client/loglevel.min.js | 2 + .../photoshop/api/extension/client/wsrpc.js | 393 ++++++ .../api/extension/client/wsrpc.min.js | 1 + .../hosts/photoshop/api/extension/host/JSX.js | 774 +++++++++++ .../photoshop/api/extension/host/index.jsx | 484 +++++++ .../photoshop/api/extension/host/json.js | 530 ++++++++ .../api/extension/icons/avalon-logo-48.png | Bin 0 -> 1362 bytes .../hosts/photoshop/api/extension/index.html | 119 ++ openpype/hosts/photoshop/api/launch_logic.py | 315 +++++ openpype/hosts/photoshop/api/lib.py | 76 ++ openpype/hosts/photoshop/api/panel.PNG | Bin 0 -> 8756 bytes .../hosts/photoshop/api/panel_failure.PNG | Bin 0 -> 13568 bytes openpype/hosts/photoshop/api/pipeline.py | 199 +++ openpype/hosts/photoshop/api/workio.py | 50 + openpype/hosts/photoshop/api/ws_stub.py | 470 +++++++ 22 files changed, 5278 insertions(+), 79 deletions(-) create mode 100644 openpype/hosts/photoshop/api/README.md create mode 100644 openpype/hosts/photoshop/api/extension.zxp create mode 100644 openpype/hosts/photoshop/api/extension/.debug create mode 100644 openpype/hosts/photoshop/api/extension/CSXS/manifest.xml create mode 100644 openpype/hosts/photoshop/api/extension/client/CSInterface.js create mode 100644 openpype/hosts/photoshop/api/extension/client/client.js create mode 100644 openpype/hosts/photoshop/api/extension/client/loglevel.min.js create mode 100644 openpype/hosts/photoshop/api/extension/client/wsrpc.js create mode 100644 openpype/hosts/photoshop/api/extension/client/wsrpc.min.js create mode 100644 openpype/hosts/photoshop/api/extension/host/JSX.js create mode 100644 openpype/hosts/photoshop/api/extension/host/index.jsx create mode 100644 openpype/hosts/photoshop/api/extension/host/json.js create mode 100644 openpype/hosts/photoshop/api/extension/icons/avalon-logo-48.png create mode 100644 openpype/hosts/photoshop/api/extension/index.html create mode 100644 openpype/hosts/photoshop/api/launch_logic.py create mode 100644 openpype/hosts/photoshop/api/lib.py create mode 100644 openpype/hosts/photoshop/api/panel.PNG create mode 100644 openpype/hosts/photoshop/api/panel_failure.PNG create mode 100644 openpype/hosts/photoshop/api/pipeline.py create mode 100644 openpype/hosts/photoshop/api/workio.py create mode 100644 openpype/hosts/photoshop/api/ws_stub.py diff --git a/openpype/hosts/photoshop/api/README.md b/openpype/hosts/photoshop/api/README.md new file mode 100644 index 00000000000..b958f538033 --- /dev/null +++ b/openpype/hosts/photoshop/api/README.md @@ -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 diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index d978d6ecc18..43756b9ee4f 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -1,79 +1,55 @@ -import os -import sys -import logging - -from Qt import QtWidgets - -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") - -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.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..") - app = QtWidgets.QApplication(sys.argv) - - 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_() - - # Garbage collect QApplication. - del app - - -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 +"""Public API + +Anything that isn't defined here is INTERNAL and unreliable for external use. + +""" + +from .pipeline import ( + ls, + list_instances, + remove_instance, + Creator, + install, + containerise +) + +from .workio import ( + file_extensions, + has_unsaved_changes, + save_file, + open_file, + current_file, + work_root, +) + +from .lib import ( + maintained_selection, + maintained_visibility +) + +from .launch_logic import stub + +__all__ = [ + # pipeline + "ls", + "list_instances", + "remove_instance", + "Creator", + "install", + "containerise", + + # workfiles + "file_extensions", + "has_unsaved_changes", + "save_file", + "open_file", + "current_file", + "work_root", + + # lib + "maintained_selection", + "maintained_visibility", + + # launch_logic + "stub" +] diff --git a/openpype/hosts/photoshop/api/extension.zxp b/openpype/hosts/photoshop/api/extension.zxp new file mode 100644 index 0000000000000000000000000000000000000000..a25ec96e7df2348e558277faedf4f74561a7f316 GIT binary patch literal 54111 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� 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¨9*zdQ|q^=Mf~ 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 z + + + + + + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml b/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml new file mode 100644 index 00000000000..6396cd24122 --- /dev/null +++ b/openpype/hosts/photoshop/api/extension/CSXS/manifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + ./index.html + + + + true + + + applicationActivate + com.adobe.csxs.events.ApplicationInitialized + + + + Panel + OpenPype + + + 300 + 140 + + + 400 + 200 + + + + ./icons/avalon-logo-48.png + + + + + + diff --git a/openpype/hosts/photoshop/api/extension/client/CSInterface.js b/openpype/hosts/photoshop/api/extension/client/CSInterface.js new file mode 100644 index 00000000000..4239391efd0 --- /dev/null +++ b/openpype/hosts/photoshop/api/extension/client/CSInterface.js @@ -0,0 +1,1193 @@ +/************************************************************************************************** +* +* ADOBE SYSTEMS INCORPORATED +* Copyright 2013 Adobe Systems Incorporated +* All Rights Reserved. +* +* NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the +* terms of the Adobe license agreement accompanying it. If you have received this file from a +* source other than Adobe, then your use, modification, or distribution of it requires the prior +* written permission of Adobe. +* +**************************************************************************************************/ + +/** CSInterface - v8.0.0 */ + +/** + * Stores constants for the window types supported by the CSXS infrastructure. + */ +function CSXSWindowType() +{ +} + +/** Constant for the CSXS window type Panel. */ +CSXSWindowType._PANEL = "Panel"; + +/** Constant for the CSXS window type Modeless. */ +CSXSWindowType._MODELESS = "Modeless"; + +/** Constant for the CSXS window type ModalDialog. */ +CSXSWindowType._MODAL_DIALOG = "ModalDialog"; + +/** EvalScript error message */ +EvalScript_ErrMessage = "EvalScript error."; + +/** + * @class Version + * Defines a version number with major, minor, micro, and special + * components. The major, minor and micro values are numeric; the special + * value can be any string. + * + * @param major The major version component, a positive integer up to nine digits long. + * @param minor The minor version component, a positive integer up to nine digits long. + * @param micro The micro version component, a positive integer up to nine digits long. + * @param special The special version component, an arbitrary string. + * + * @return A new \c Version object. + */ +function Version(major, minor, micro, special) +{ + this.major = major; + this.minor = minor; + this.micro = micro; + this.special = special; +} + +/** + * The maximum value allowed for a numeric version component. + * This reflects the maximum value allowed in PlugPlug and the manifest schema. + */ +Version.MAX_NUM = 999999999; + +/** + * @class VersionBound + * Defines a boundary for a version range, which associates a \c Version object + * with a flag for whether it is an inclusive or exclusive boundary. + * + * @param version The \c #Version object. + * @param inclusive True if this boundary is inclusive, false if it is exclusive. + * + * @return A new \c VersionBound object. + */ +function VersionBound(version, inclusive) +{ + this.version = version; + this.inclusive = inclusive; +} + +/** + * @class VersionRange + * Defines a range of versions using a lower boundary and optional upper boundary. + * + * @param lowerBound The \c #VersionBound object. + * @param upperBound The \c #VersionBound object, or null for a range with no upper boundary. + * + * @return A new \c VersionRange object. + */ +function VersionRange(lowerBound, upperBound) +{ + this.lowerBound = lowerBound; + this.upperBound = upperBound; +} + +/** + * @class Runtime + * Represents a runtime related to the CEP infrastructure. + * Extensions can declare dependencies on particular + * CEP runtime versions in the extension manifest. + * + * @param name The runtime name. + * @param version A \c #VersionRange object that defines a range of valid versions. + * + * @return A new \c Runtime object. + */ +function Runtime(name, versionRange) +{ + this.name = name; + this.versionRange = versionRange; +} + +/** +* @class Extension +* Encapsulates a CEP-based extension to an Adobe application. +* +* @param id The unique identifier of this extension. +* @param name The localizable display name of this extension. +* @param mainPath The path of the "index.html" file. +* @param basePath The base path of this extension. +* @param windowType The window type of the main window of this extension. + Valid values are defined by \c #CSXSWindowType. +* @param width The default width in pixels of the main window of this extension. +* @param height The default height in pixels of the main window of this extension. +* @param minWidth The minimum width in pixels of the main window of this extension. +* @param minHeight The minimum height in pixels of the main window of this extension. +* @param maxWidth The maximum width in pixels of the main window of this extension. +* @param maxHeight The maximum height in pixels of the main window of this extension. +* @param defaultExtensionDataXml The extension data contained in the default \c ExtensionDispatchInfo section of the extension manifest. +* @param specialExtensionDataXml The extension data contained in the application-specific \c ExtensionDispatchInfo section of the extension manifest. +* @param requiredRuntimeList An array of \c Runtime objects for runtimes required by this extension. +* @param isAutoVisible True if this extension is visible on loading. +* @param isPluginExtension True if this extension has been deployed in the Plugins folder of the host application. +* +* @return A new \c Extension object. +*/ +function Extension(id, name, mainPath, basePath, windowType, width, height, minWidth, minHeight, maxWidth, maxHeight, + defaultExtensionDataXml, specialExtensionDataXml, requiredRuntimeList, isAutoVisible, isPluginExtension) +{ + this.id = id; + this.name = name; + this.mainPath = mainPath; + this.basePath = basePath; + this.windowType = windowType; + this.width = width; + this.height = height; + this.minWidth = minWidth; + this.minHeight = minHeight; + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + this.defaultExtensionDataXml = defaultExtensionDataXml; + this.specialExtensionDataXml = specialExtensionDataXml; + this.requiredRuntimeList = requiredRuntimeList; + this.isAutoVisible = isAutoVisible; + this.isPluginExtension = isPluginExtension; +} + +/** + * @class CSEvent + * A standard JavaScript event, the base class for CEP events. + * + * @param type The name of the event type. + * @param scope The scope of event, can be "GLOBAL" or "APPLICATION". + * @param appId The unique identifier of the application that generated the event. + * @param extensionId The unique identifier of the extension that generated the event. + * + * @return A new \c CSEvent object + */ +function CSEvent(type, scope, appId, extensionId) +{ + this.type = type; + this.scope = scope; + this.appId = appId; + this.extensionId = extensionId; +} + +/** Event-specific data. */ +CSEvent.prototype.data = ""; + +/** + * @class SystemPath + * Stores operating-system-specific location constants for use in the + * \c #CSInterface.getSystemPath() method. + * @return A new \c SystemPath object. + */ +function SystemPath() +{ +} + +/** The path to user data. */ +SystemPath.USER_DATA = "userData"; + +/** The path to common files for Adobe applications. */ +SystemPath.COMMON_FILES = "commonFiles"; + +/** The path to the user's default document folder. */ +SystemPath.MY_DOCUMENTS = "myDocuments"; + +/** @deprecated. Use \c #SystemPath.Extension. */ +SystemPath.APPLICATION = "application"; + +/** The path to current extension. */ +SystemPath.EXTENSION = "extension"; + +/** The path to hosting application's executable. */ +SystemPath.HOST_APPLICATION = "hostApplication"; + +/** + * @class ColorType + * Stores color-type constants. + */ +function ColorType() +{ +} + +/** RGB color type. */ +ColorType.RGB = "rgb"; + +/** Gradient color type. */ +ColorType.GRADIENT = "gradient"; + +/** Null color type. */ +ColorType.NONE = "none"; + +/** + * @class RGBColor + * Stores an RGB color with red, green, blue, and alpha values. + * All values are in the range [0.0 to 255.0]. Invalid numeric values are + * converted to numbers within this range. + * + * @param red The red value, in the range [0.0 to 255.0]. + * @param green The green value, in the range [0.0 to 255.0]. + * @param blue The blue value, in the range [0.0 to 255.0]. + * @param alpha The alpha (transparency) value, in the range [0.0 to 255.0]. + * The default, 255.0, means that the color is fully opaque. + * + * @return A new RGBColor object. + */ +function RGBColor(red, green, blue, alpha) +{ + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; +} + +/** + * @class Direction + * A point value in which the y component is 0 and the x component + * is positive or negative for a right or left direction, + * or the x component is 0 and the y component is positive or negative for + * an up or down direction. + * + * @param x The horizontal component of the point. + * @param y The vertical component of the point. + * + * @return A new \c Direction object. + */ +function Direction(x, y) +{ + this.x = x; + this.y = y; +} + +/** + * @class GradientStop + * Stores gradient stop information. + * + * @param offset The offset of the gradient stop, in the range [0.0 to 1.0]. + * @param rgbColor The color of the gradient at this point, an \c #RGBColor object. + * + * @return GradientStop object. + */ +function GradientStop(offset, rgbColor) +{ + this.offset = offset; + this.rgbColor = rgbColor; +} + +/** + * @class GradientColor + * Stores gradient color information. + * + * @param type The gradient type, must be "linear". + * @param direction A \c #Direction object for the direction of the gradient + (up, down, right, or left). + * @param numStops The number of stops in the gradient. + * @param gradientStopList An array of \c #GradientStop objects. + * + * @return A new \c GradientColor object. + */ +function GradientColor(type, direction, numStops, arrGradientStop) +{ + this.type = type; + this.direction = direction; + this.numStops = numStops; + this.arrGradientStop = arrGradientStop; +} + +/** + * @class UIColor + * Stores color information, including the type, anti-alias level, and specific color + * values in a color object of an appropriate type. + * + * @param type The color type, 1 for "rgb" and 2 for "gradient". + The supplied color object must correspond to this type. + * @param antialiasLevel The anti-alias level constant. + * @param color A \c #RGBColor or \c #GradientColor object containing specific color information. + * + * @return A new \c UIColor object. + */ +function UIColor(type, antialiasLevel, color) +{ + this.type = type; + this.antialiasLevel = antialiasLevel; + this.color = color; +} + +/** + * @class AppSkinInfo + * Stores window-skin properties, such as color and font. All color parameter values are \c #UIColor objects except that systemHighlightColor is \c #RGBColor object. + * + * @param baseFontFamily The base font family of the application. + * @param baseFontSize The base font size of the application. + * @param appBarBackgroundColor The application bar background color. + * @param panelBackgroundColor The background color of the extension panel. + * @param appBarBackgroundColorSRGB The application bar background color, as sRGB. + * @param panelBackgroundColorSRGB The background color of the extension panel, as sRGB. + * @param systemHighlightColor The highlight color of the extension panel, if provided by the host application. Otherwise, the operating-system highlight color. + * + * @return AppSkinInfo object. + */ +function AppSkinInfo(baseFontFamily, baseFontSize, appBarBackgroundColor, panelBackgroundColor, appBarBackgroundColorSRGB, panelBackgroundColorSRGB, systemHighlightColor) +{ + this.baseFontFamily = baseFontFamily; + this.baseFontSize = baseFontSize; + this.appBarBackgroundColor = appBarBackgroundColor; + this.panelBackgroundColor = panelBackgroundColor; + this.appBarBackgroundColorSRGB = appBarBackgroundColorSRGB; + this.panelBackgroundColorSRGB = panelBackgroundColorSRGB; + this.systemHighlightColor = systemHighlightColor; +} + +/** + * @class HostEnvironment + * Stores information about the environment in which the extension is loaded. + * + * @param appName The application's name. + * @param appVersion The application's version. + * @param appLocale The application's current license locale. + * @param appUILocale The application's current UI locale. + * @param appId The application's unique identifier. + * @param isAppOnline True if the application is currently online. + * @param appSkinInfo An \c #AppSkinInfo object containing the application's default color and font styles. + * + * @return A new \c HostEnvironment object. + */ +function HostEnvironment(appName, appVersion, appLocale, appUILocale, appId, isAppOnline, appSkinInfo) +{ + this.appName = appName; + this.appVersion = appVersion; + this.appLocale = appLocale; + this.appUILocale = appUILocale; + this.appId = appId; + this.isAppOnline = isAppOnline; + this.appSkinInfo = appSkinInfo; +} + +/** + * @class HostCapabilities + * Stores information about the host capabilities. + * + * @param EXTENDED_PANEL_MENU True if the application supports panel menu. + * @param EXTENDED_PANEL_ICONS True if the application supports panel icon. + * @param DELEGATE_APE_ENGINE True if the application supports delegated APE engine. + * @param SUPPORT_HTML_EXTENSIONS True if the application supports HTML extensions. + * @param DISABLE_FLASH_EXTENSIONS True if the application disables FLASH extensions. + * + * @return A new \c HostCapabilities object. + */ +function HostCapabilities(EXTENDED_PANEL_MENU, EXTENDED_PANEL_ICONS, DELEGATE_APE_ENGINE, SUPPORT_HTML_EXTENSIONS, DISABLE_FLASH_EXTENSIONS) +{ + this.EXTENDED_PANEL_MENU = EXTENDED_PANEL_MENU; + this.EXTENDED_PANEL_ICONS = EXTENDED_PANEL_ICONS; + this.DELEGATE_APE_ENGINE = DELEGATE_APE_ENGINE; + this.SUPPORT_HTML_EXTENSIONS = SUPPORT_HTML_EXTENSIONS; + this.DISABLE_FLASH_EXTENSIONS = DISABLE_FLASH_EXTENSIONS; // Since 5.0.0 +} + +/** + * @class ApiVersion + * Stores current api version. + * + * Since 4.2.0 + * + * @param major The major version + * @param minor The minor version. + * @param micro The micro version. + * + * @return ApiVersion object. + */ +function ApiVersion(major, minor, micro) +{ + this.major = major; + this.minor = minor; + this.micro = micro; +} + +/** + * @class MenuItemStatus + * Stores flyout menu item status + * + * Since 5.2.0 + * + * @param menuItemLabel The menu item label. + * @param enabled True if user wants to enable the menu item. + * @param checked True if user wants to check the menu item. + * + * @return MenuItemStatus object. + */ +function MenuItemStatus(menuItemLabel, enabled, checked) +{ + this.menuItemLabel = menuItemLabel; + this.enabled = enabled; + this.checked = checked; +} + +/** + * @class ContextMenuItemStatus + * Stores the status of the context menu item. + * + * Since 5.2.0 + * + * @param menuItemID The menu item id. + * @param enabled True if user wants to enable the menu item. + * @param checked True if user wants to check the menu item. + * + * @return MenuItemStatus object. + */ +function ContextMenuItemStatus(menuItemID, enabled, checked) +{ + this.menuItemID = menuItemID; + this.enabled = enabled; + this.checked = checked; +} +//------------------------------ CSInterface ---------------------------------- + +/** + * @class CSInterface + * This is the entry point to the CEP extensibility infrastructure. + * Instantiate this object and use it to: + *
    + *
  • Access information about the host application in which an extension is running
  • + *
  • Launch an extension
  • + *
  • Register interest in event notifications, and dispatch events
  • + *
+ * + * @return A new \c CSInterface object + */ +function CSInterface() +{ +} + +/** + * User can add this event listener to handle native application theme color changes. + * Callback function gives extensions ability to fine-tune their theme color after the + * global theme color has been changed. + * The callback function should be like below: + * + * @example + * // event is a CSEvent object, but user can ignore it. + * function OnAppThemeColorChanged(event) + * { + * // Should get a latest HostEnvironment object from application. + * var skinInfo = JSON.parse(window.__adobe_cep__.getHostEnvironment()).appSkinInfo; + * // Gets the style information such as color info from the skinInfo, + * // and redraw all UI controls of your extension according to the style info. + * } + */ +CSInterface.THEME_COLOR_CHANGED_EVENT = "com.adobe.csxs.events.ThemeColorChanged"; + +/** The host environment data object. */ +CSInterface.prototype.hostEnvironment = window.__adobe_cep__ ? JSON.parse(window.__adobe_cep__.getHostEnvironment()) : null; + +/** Retrieves information about the host environment in which the + * extension is currently running. + * + * @return A \c #HostEnvironment object. + */ +CSInterface.prototype.getHostEnvironment = function() +{ + this.hostEnvironment = JSON.parse(window.__adobe_cep__.getHostEnvironment()); + return this.hostEnvironment; +}; + +/** Closes this extension. */ +CSInterface.prototype.closeExtension = function() +{ + window.__adobe_cep__.closeExtension(); +}; + +/** + * Retrieves a path for which a constant is defined in the system. + * + * @param pathType The path-type constant defined in \c #SystemPath , + * + * @return The platform-specific system path string. + */ +CSInterface.prototype.getSystemPath = function(pathType) +{ + var path = decodeURI(window.__adobe_cep__.getSystemPath(pathType)); + var OSVersion = this.getOSInformation(); + if (OSVersion.indexOf("Windows") >= 0) + { + path = path.replace("file:///", ""); + } + else if (OSVersion.indexOf("Mac") >= 0) + { + path = path.replace("file://", ""); + } + return path; +}; + +/** + * Evaluates a JavaScript script, which can use the JavaScript DOM + * of the host application. + * + * @param script The JavaScript script. + * @param callback Optional. A callback function that receives the result of execution. + * If execution fails, the callback function receives the error message \c EvalScript_ErrMessage. + */ +CSInterface.prototype.evalScript = function(script, callback) +{ + if(callback === null || callback === undefined) + { + callback = function(result){}; + } + window.__adobe_cep__.evalScript(script, callback); +}; + +/** + * Retrieves the unique identifier of the application. + * in which the extension is currently running. + * + * @return The unique ID string. + */ +CSInterface.prototype.getApplicationID = function() +{ + var appId = this.hostEnvironment.appId; + return appId; +}; + +/** + * Retrieves host capability information for the application + * in which the extension is currently running. + * + * @return A \c #HostCapabilities object. + */ +CSInterface.prototype.getHostCapabilities = function() +{ + var hostCapabilities = JSON.parse(window.__adobe_cep__.getHostCapabilities() ); + return hostCapabilities; +}; + +/** + * Triggers a CEP event programmatically. Yoy can use it to dispatch + * an event of a predefined type, or of a type you have defined. + * + * @param event A \c CSEvent object. + */ +CSInterface.prototype.dispatchEvent = function(event) +{ + if (typeof event.data == "object") + { + event.data = JSON.stringify(event.data); + } + + window.__adobe_cep__.dispatchEvent(event); +}; + +/** + * Registers an interest in a CEP event of a particular type, and + * assigns an event handler. + * The event infrastructure notifies your extension when events of this type occur, + * passing the event object to the registered handler function. + * + * @param type The name of the event type of interest. + * @param listener The JavaScript handler function or method. + * @param obj Optional, the object containing the handler method, if any. + * Default is null. + */ +CSInterface.prototype.addEventListener = function(type, listener, obj) +{ + window.__adobe_cep__.addEventListener(type, listener, obj); +}; + +/** + * Removes a registered event listener. + * + * @param type The name of the event type of interest. + * @param listener The JavaScript handler function or method that was registered. + * @param obj Optional, the object containing the handler method, if any. + * Default is null. + */ +CSInterface.prototype.removeEventListener = function(type, listener, obj) +{ + window.__adobe_cep__.removeEventListener(type, listener, obj); +}; + +/** + * Loads and launches another extension, or activates the extension if it is already loaded. + * + * @param extensionId The extension's unique identifier. + * @param startupParams Not currently used, pass "". + * + * @example + * To launch the extension "help" with ID "HLP" from this extension, call: + * requestOpenExtension("HLP", ""); + * + */ +CSInterface.prototype.requestOpenExtension = function(extensionId, params) +{ + window.__adobe_cep__.requestOpenExtension(extensionId, params); +}; + +/** + * Retrieves the list of extensions currently loaded in the current host application. + * The extension list is initialized once, and remains the same during the lifetime + * of the CEP session. + * + * @param extensionIds Optional, an array of unique identifiers for extensions of interest. + * If omitted, retrieves data for all extensions. + * + * @return Zero or more \c #Extension objects. + */ +CSInterface.prototype.getExtensions = function(extensionIds) +{ + var extensionIdsStr = JSON.stringify(extensionIds); + var extensionsStr = window.__adobe_cep__.getExtensions(extensionIdsStr); + + var extensions = JSON.parse(extensionsStr); + return extensions; +}; + +/** + * Retrieves network-related preferences. + * + * @return A JavaScript object containing network preferences. + */ +CSInterface.prototype.getNetworkPreferences = function() +{ + var result = window.__adobe_cep__.getNetworkPreferences(); + var networkPre = JSON.parse(result); + + return networkPre; +}; + +/** + * Initializes the resource bundle for this extension with property values + * for the current application and locale. + * To support multiple locales, you must define a property file for each locale, + * containing keyed display-string values for that locale. + * See localization documentation for Extension Builder and related products. + * + * Keys can be in the + * form key.value="localized string", for use in HTML text elements. + * For example, in this input element, the localized \c key.value string is displayed + * instead of the empty \c value string: + * + * + * + * @return An object containing the resource bundle information. + */ +CSInterface.prototype.initResourceBundle = function() +{ + var resourceBundle = JSON.parse(window.__adobe_cep__.initResourceBundle()); + var resElms = document.querySelectorAll('[data-locale]'); + for (var n = 0; n < resElms.length; n++) + { + var resEl = resElms[n]; + // Get the resource key from the element. + var resKey = resEl.getAttribute('data-locale'); + if (resKey) + { + // Get all the resources that start with the key. + for (var key in resourceBundle) + { + if (key.indexOf(resKey) === 0) + { + var resValue = resourceBundle[key]; + if (key.length == resKey.length) + { + resEl.innerHTML = resValue; + } + else if ('.' == key.charAt(resKey.length)) + { + var attrKey = key.substring(resKey.length + 1); + resEl[attrKey] = resValue; + } + } + } + } + } + return resourceBundle; +}; + +/** + * Writes installation information to a file. + * + * @return The file path. + */ +CSInterface.prototype.dumpInstallationInfo = function() +{ + return window.__adobe_cep__.dumpInstallationInfo(); +}; + +/** + * Retrieves version information for the current Operating System, + * See http://www.useragentstring.com/pages/Chrome/ for Chrome \c navigator.userAgent values. + * + * @return A string containing the OS version, or "unknown Operation System". + * If user customizes the User Agent by setting CEF command parameter "--user-agent", only + * "Mac OS X" or "Windows" will be returned. + */ +CSInterface.prototype.getOSInformation = function() +{ + var userAgent = navigator.userAgent; + + if ((navigator.platform == "Win32") || (navigator.platform == "Windows")) + { + var winVersion = "Windows"; + var winBit = ""; + if (userAgent.indexOf("Windows") > -1) + { + if (userAgent.indexOf("Windows NT 5.0") > -1) + { + winVersion = "Windows 2000"; + } + else if (userAgent.indexOf("Windows NT 5.1") > -1) + { + winVersion = "Windows XP"; + } + else if (userAgent.indexOf("Windows NT 5.2") > -1) + { + winVersion = "Windows Server 2003"; + } + else if (userAgent.indexOf("Windows NT 6.0") > -1) + { + winVersion = "Windows Vista"; + } + else if (userAgent.indexOf("Windows NT 6.1") > -1) + { + winVersion = "Windows 7"; + } + else if (userAgent.indexOf("Windows NT 6.2") > -1) + { + winVersion = "Windows 8"; + } + else if (userAgent.indexOf("Windows NT 6.3") > -1) + { + winVersion = "Windows 8.1"; + } + else if (userAgent.indexOf("Windows NT 10") > -1) + { + winVersion = "Windows 10"; + } + + if (userAgent.indexOf("WOW64") > -1 || userAgent.indexOf("Win64") > -1) + { + winBit = " 64-bit"; + } + else + { + winBit = " 32-bit"; + } + } + + return winVersion + winBit; + } + else if ((navigator.platform == "MacIntel") || (navigator.platform == "Macintosh")) + { + var result = "Mac OS X"; + + if (userAgent.indexOf("Mac OS X") > -1) + { + result = userAgent.substring(userAgent.indexOf("Mac OS X"), userAgent.indexOf(")")); + result = result.replace(/_/g, "."); + } + + return result; + } + + return "Unknown Operation System"; +}; + +/** + * Opens a page in the default system browser. + * + * Since 4.2.0 + * + * @param url The URL of the page/file to open, or the email address. + * Must use HTTP/HTTPS/file/mailto protocol. For example: + * "http://www.adobe.com" + * "https://github.com" + * "file:///C:/log.txt" + * "mailto:test@adobe.com" + * + * @return One of these error codes:\n + *
    \n + *
  • NO_ERROR - 0
  • \n + *
  • ERR_UNKNOWN - 1
  • \n + *
  • ERR_INVALID_PARAMS - 2
  • \n + *
  • ERR_INVALID_URL - 201
  • \n + *
\n + */ +CSInterface.prototype.openURLInDefaultBrowser = function(url) +{ + return cep.util.openURLInDefaultBrowser(url); +}; + +/** + * Retrieves extension ID. + * + * Since 4.2.0 + * + * @return extension ID. + */ +CSInterface.prototype.getExtensionID = function() +{ + return window.__adobe_cep__.getExtensionId(); +}; + +/** + * Retrieves the scale factor of screen. + * On Windows platform, the value of scale factor might be different from operating system's scale factor, + * since host application may use its self-defined scale factor. + * + * Since 4.2.0 + * + * @return One of the following float number. + *
    \n + *
  • -1.0 when error occurs
  • \n + *
  • 1.0 means normal screen
  • \n + *
  • >1.0 means HiDPI screen
  • \n + *
\n + */ +CSInterface.prototype.getScaleFactor = function() +{ + return window.__adobe_cep__.getScaleFactor(); +}; + +/** + * Set a handler to detect any changes of scale factor. This only works on Mac. + * + * Since 4.2.0 + * + * @param handler The function to be called when scale factor is changed. + * + */ +CSInterface.prototype.setScaleFactorChangedHandler = function(handler) +{ + window.__adobe_cep__.setScaleFactorChangedHandler(handler); +}; + +/** + * Retrieves current API version. + * + * Since 4.2.0 + * + * @return ApiVersion object. + * + */ +CSInterface.prototype.getCurrentApiVersion = function() +{ + var apiVersion = JSON.parse(window.__adobe_cep__.getCurrentApiVersion()); + return apiVersion; +}; + +/** + * Set panel flyout menu by an XML. + * + * Since 5.2.0 + * + * Register a callback function for "com.adobe.csxs.events.flyoutMenuClicked" to get notified when a + * menu item is clicked. + * The "data" attribute of event is an object which contains "menuId" and "menuName" attributes. + * + * Register callback functions for "com.adobe.csxs.events.flyoutMenuOpened" and "com.adobe.csxs.events.flyoutMenuClosed" + * respectively to get notified when flyout menu is opened or closed. + * + * @param menu A XML string which describes menu structure. + * An example menu XML: + * + * + * + * + * + * + * + * + * + * + * + * + */ +CSInterface.prototype.setPanelFlyoutMenu = function(menu) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeSync("setPanelFlyoutMenu", menu); +}; + +/** + * Updates a menu item in the extension window's flyout menu, by setting the enabled + * and selection status. + * + * Since 5.2.0 + * + * @param menuItemLabel The menu item label. + * @param enabled True to enable the item, false to disable it (gray it out). + * @param checked True to select the item, false to deselect it. + * + * @return false when the host application does not support this functionality (HostCapabilities.EXTENDED_PANEL_MENU is false). + * Fails silently if menu label is invalid. + * + * @see HostCapabilities.EXTENDED_PANEL_MENU + */ +CSInterface.prototype.updatePanelMenuItem = function(menuItemLabel, enabled, checked) +{ + var ret = false; + if (this.getHostCapabilities().EXTENDED_PANEL_MENU) + { + var itemStatus = new MenuItemStatus(menuItemLabel, enabled, checked); + ret = window.__adobe_cep__.invokeSync("updatePanelMenuItem", JSON.stringify(itemStatus)); + } + return ret; +}; + + +/** + * Set context menu by XML string. + * + * Since 5.2.0 + * + * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. + * - an item without menu ID or menu name is disabled and is not shown. + * - if the item name is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. + * - Checkable attribute takes precedence over Checked attribute. + * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. + The Chrome extension contextMenus API was taken as a reference. + https://developer.chrome.com/extensions/contextMenus + * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. + * + * @param menu A XML string which describes menu structure. + * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. + * + * @description An example menu XML: + * + * + * + * + * + * + * + * + * + * + * + */ +CSInterface.prototype.setContextMenu = function(menu, callback) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeAsync("setContextMenu", menu, callback); +}; + +/** + * Set context menu by JSON string. + * + * Since 6.0.0 + * + * There are a number of conventions used to communicate what type of menu item to create and how it should be handled. + * - an item without menu ID or menu name is disabled and is not shown. + * - if the item label is "---" (three hyphens) then it is treated as a separator. The menu ID in this case will always be NULL. + * - Checkable attribute takes precedence over Checked attribute. + * - a PNG icon. For optimal display results please supply a 16 x 16px icon as larger dimensions will increase the size of the menu item. + The Chrome extension contextMenus API was taken as a reference. + * - the items with icons and checkable items cannot coexist on the same menu level. The former take precedences over the latter. + https://developer.chrome.com/extensions/contextMenus + * + * @param menu A JSON string which describes menu structure. + * @param callback The callback function which is called when a menu item is clicked. The only parameter is the returned ID of clicked menu item. + * + * @description An example menu JSON: + * + * { + * "menu": [ + * { + * "id": "menuItemId1", + * "label": "testExample1", + * "enabled": true, + * "checkable": true, + * "checked": false, + * "icon": "./image/small_16X16.png" + * }, + * { + * "id": "menuItemId2", + * "label": "testExample2", + * "menu": [ + * { + * "id": "menuItemId2-1", + * "label": "testExample2-1", + * "menu": [ + * { + * "id": "menuItemId2-1-1", + * "label": "testExample2-1-1", + * "enabled": false, + * "checkable": true, + * "checked": true + * } + * ] + * }, + * { + * "id": "menuItemId2-2", + * "label": "testExample2-2", + * "enabled": true, + * "checkable": true, + * "checked": true + * } + * ] + * }, + * { + * "label": "---" + * }, + * { + * "id": "menuItemId3", + * "label": "testExample3", + * "enabled": false, + * "checkable": true, + * "checked": false + * } + * ] + * } + * + */ +CSInterface.prototype.setContextMenuByJSON = function(menu, callback) +{ + if ("string" != typeof menu) + { + return; + } + + window.__adobe_cep__.invokeAsync("setContextMenuByJSON", menu, callback); +}; + +/** + * Updates a context menu item by setting the enabled and selection status. + * + * Since 5.2.0 + * + * @param menuItemID The menu item ID. + * @param enabled True to enable the item, false to disable it (gray it out). + * @param checked True to select the item, false to deselect it. + */ +CSInterface.prototype.updateContextMenuItem = function(menuItemID, enabled, checked) +{ + var itemStatus = new ContextMenuItemStatus(menuItemID, enabled, checked); + ret = window.__adobe_cep__.invokeSync("updateContextMenuItem", JSON.stringify(itemStatus)); +}; + +/** + * Get the visibility status of an extension window. + * + * Since 6.0.0 + * + * @return true if the extension window is visible; false if the extension window is hidden. + */ +CSInterface.prototype.isWindowVisible = function() +{ + return window.__adobe_cep__.invokeSync("isWindowVisible", ""); +}; + +/** + * Resize extension's content to the specified dimensions. + * 1. Works with modal and modeless extensions in all Adobe products. + * 2. Extension's manifest min/max size constraints apply and take precedence. + * 3. For panel extensions + * 3.1 This works in all Adobe products except: + * * Premiere Pro + * * Prelude + * * After Effects + * 3.2 When the panel is in certain states (especially when being docked), + * it will not change to the desired dimensions even when the + * specified size satisfies min/max constraints. + * + * Since 6.0.0 + * + * @param width The new width + * @param height The new height + */ +CSInterface.prototype.resizeContent = function(width, height) +{ + window.__adobe_cep__.resizeContent(width, height); +}; + +/** + * Register the invalid certificate callback for an extension. + * This callback will be triggered when the extension tries to access the web site that contains the invalid certificate on the main frame. + * But if the extension does not call this function and tries to access the web site containing the invalid certificate, a default error page will be shown. + * + * Since 6.1.0 + * + * @param callback the callback function + */ +CSInterface.prototype.registerInvalidCertificateCallback = function(callback) +{ + return window.__adobe_cep__.registerInvalidCertificateCallback(callback); +}; + +/** + * Register an interest in some key events to prevent them from being sent to the host application. + * + * This function works with modeless extensions and panel extensions. + * Generally all the key events will be sent to the host application for these two extensions if the current focused element + * is not text input or dropdown, + * If you want to intercept some key events and want them to be handled in the extension, please call this function + * in advance to prevent them being sent to the host application. + * + * Since 6.1.0 + * + * @param keyEventsInterest A JSON string describing those key events you are interested in. A null object or + an empty string will lead to removing the interest + * + * This JSON string should be an array, each object has following keys: + * + * keyCode: [Required] represents an OS system dependent virtual key code identifying + * the unmodified value of the pressed key. + * ctrlKey: [optional] a Boolean that indicates if the control key was pressed (true) or not (false) when the event occurred. + * altKey: [optional] a Boolean that indicates if the alt key was pressed (true) or not (false) when the event occurred. + * shiftKey: [optional] a Boolean that indicates if the shift key was pressed (true) or not (false) when the event occurred. + * metaKey: [optional] (Mac Only) a Boolean that indicates if the Meta key was pressed (true) or not (false) when the event occurred. + * On Macintosh keyboards, this is the command key. To detect Windows key on Windows, please use keyCode instead. + * An example JSON string: + * + * [ + * { + * "keyCode": 48 + * }, + * { + * "keyCode": 123, + * "ctrlKey": true + * }, + * { + * "keyCode": 123, + * "ctrlKey": true, + * "metaKey": true + * } + * ] + * + */ +CSInterface.prototype.registerKeyEventsInterest = function(keyEventsInterest) +{ + return window.__adobe_cep__.registerKeyEventsInterest(keyEventsInterest); +}; + +/** + * Set the title of the extension window. + * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. + * + * Since 6.1.0 + * + * @param title The window title. + */ +CSInterface.prototype.setWindowTitle = function(title) +{ + window.__adobe_cep__.invokeSync("setWindowTitle", title); +}; + +/** + * Get the title of the extension window. + * This function works with modal and modeless extensions in all Adobe products, and panel extensions in Photoshop, InDesign, InCopy, Illustrator, Flash Pro and Dreamweaver. + * + * Since 6.1.0 + * + * @return The window title. + */ +CSInterface.prototype.getWindowTitle = function() +{ + return window.__adobe_cep__.invokeSync("getWindowTitle", ""); +}; diff --git a/openpype/hosts/photoshop/api/extension/client/client.js b/openpype/hosts/photoshop/api/extension/client/client.js new file mode 100644 index 00000000000..f4ba4cfe47b --- /dev/null +++ b/openpype/hosts/photoshop/api/extension/client/client.js @@ -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"); diff --git a/openpype/hosts/photoshop/api/extension/client/loglevel.min.js b/openpype/hosts/photoshop/api/extension/client/loglevel.min.js new file mode 100644 index 00000000000..648d7e9ff62 --- /dev/null +++ b/openpype/hosts/photoshop/api/extension/client/loglevel.min.js @@ -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=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 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 diff --git a/openpype/hosts/photoshop/api/extension/client/wsrpc.min.js b/openpype/hosts/photoshop/api/extension/client/wsrpc.min.js new file mode 100644 index 00000000000..f1264b91c4e --- /dev/null +++ b/openpype/hosts/photoshop/api/extension/client/wsrpc.min.js @@ -0,0 +1 @@ +!function(global,factory){"object"==typeof exports&&"undefined"!=typeof module?module.exports=factory():"function"==typeof define&&define.amd?define(factory):(global=global||self).WSRPC=factory()}(this,function(){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}function Deferred(){_classCallCheck(this,Deferred);var self=this;function wrapper(func){return function(){if(!self.done)return self.done=!0,func.apply(this,arguments);console.error(new Error("Promise already done"))}}return self.resolve=null,self.reject=null,self.done=!1,self.promise=new Promise(function(resolve,reject){self.resolve=wrapper(resolve),self.reject=wrapper(reject)}),self.promise.isPending=function(){return!self.done},self}function logGroup(group,level,args){console.group(group),console[level].apply(this,args),console.groupEnd()}function log(){WSRPC.DEBUG&&logGroup("WSRPC.DEBUG","trace",arguments)}function trace(msg){if(WSRPC.TRACE){var payload=msg;"data"in msg&&(payload=JSON.parse(msg.data)),logGroup("WSRPC.TRACE","trace",[payload])}}var readyState=Object.freeze({0:"CONNECTING",1:"OPEN",2:"CLOSING",3:"CLOSED"}),WSRPC=function WSRPC(URL){var reconnectTimeout=1 // +// 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) // +// 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 + * 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 + * 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 + 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 // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + + jsx = new Jsx(); +})(); diff --git a/openpype/hosts/photoshop/api/extension/host/index.jsx b/openpype/hosts/photoshop/api/extension/host/index.jsx new file mode 100644 index 00000000000..2acec1ebc13 --- /dev/null +++ b/openpype/hosts/photoshop/api/extension/host/index.jsx @@ -0,0 +1,484 @@ +#include "json.js"; +#target photoshop + +var LogFactory=function(file,write,store,level,defaultStatus,continuing){if(file&&(file.constructor===String||file.constructor===File)){file={file:file};}else if(!file)file={file:{}};write=(file.write!==undefined)?file.write:write;if(write===undefined){write=true;}store=(file.store!==undefined)?file.store||false:store||false;level=(file.level!==undefined)?file.level:level;defaultStatus=(file.defaultStatus!==undefined)?file.defaultStatus:defaultStatus;if(defaultStatus===undefined){defaultStatus='LOG';}continuing=(file.continuing!==undefined)?file.continuing:continuing||false;file=file.file||{};var stack,times,logTime,logPoint,icons,statuses,LOG_LEVEL,LOG_STATUS;stack=[];times=[];logTime=new Date();logPoint='Log Factory Start';icons={"1":"\ud83d\udd50","130":"\ud83d\udd5c","2":"\ud83d\udd51","230":"\ud83d\udd5d","3":"\ud83d\udd52","330":"\ud83d\udd5e","4":"\ud83d\udd53","430":"\ud83d\udd5f","5":"\ud83d\udd54","530":"\ud83d\udd60","6":"\ud83d\udd55","630":"\ud83d\udd61","7":"\ud83d\udd56","730":"\ud83d\udd62","8":"\ud83d\udd57","830":"\ud83d\udd63","9":"\ud83d\udd58","930":"\ud83d\udd64","10":"\ud83d\udd59","1030":"\ud83d\udd65","11":"\ud83d\udd5a","1130":"\ud83d\udd66","12":"\ud83d\udd5b","1230":"\ud83d\udd67","AIRPLANE":"\ud83d\udee9","ALARM":"\u23f0","AMBULANCE":"\ud83d\ude91","ANCHOR":"\u2693","ANGRY":"\ud83d\ude20","ANGUISHED":"\ud83d\ude27","ANT":"\ud83d\udc1c","ANTENNA":"\ud83d\udce1","APPLE":"\ud83c\udf4f","APPLE2":"\ud83c\udf4e","ATM":"\ud83c\udfe7","ATOM":"\u269b","BABYBOTTLE":"\ud83c\udf7c","BAD:":"\ud83d\udc4e","BANANA":"\ud83c\udf4c","BANDAGE":"\ud83e\udd15","BANK":"\ud83c\udfe6","BATTERY":"\ud83d\udd0b","BED":"\ud83d\udecf","BEE":"\ud83d\udc1d","BEER":"\ud83c\udf7a","BELL":"\ud83d\udd14","BELLOFF":"\ud83d\udd15","BIRD":"\ud83d\udc26","BLACKFLAG":"\ud83c\udff4","BLUSH":"\ud83d\ude0a","BOMB":"\ud83d\udca3","BOOK":"\ud83d\udcd5","BOOKMARK":"\ud83d\udd16","BOOKS":"\ud83d\udcda","BOW":"\ud83c\udff9","BOWLING":"\ud83c\udfb3","BRIEFCASE":"\ud83d\udcbc","BROKEN":"\ud83d\udc94","BUG":"\ud83d\udc1b","BUILDING":"\ud83c\udfdb","BUILDINGS":"\ud83c\udfd8","BULB":"\ud83d\udca1","BUS":"\ud83d\ude8c","CACTUS":"\ud83c\udf35","CALENDAR":"\ud83d\udcc5","CAMEL":"\ud83d\udc2a","CAMERA":"\ud83d\udcf7","CANDLE":"\ud83d\udd6f","CAR":"\ud83d\ude98","CAROUSEL":"\ud83c\udfa0","CASTLE":"\ud83c\udff0","CATEYES":"\ud83d\ude3b","CATJOY":"\ud83d\ude39","CATMOUTH":"\ud83d\ude3a","CATSMILE":"\ud83d\ude3c","CD":"\ud83d\udcbf","CHECK":"\u2714","CHEQFLAG":"\ud83c\udfc1","CHICK":"\ud83d\udc25","CHICKEN":"\ud83d\udc14","CHICKHEAD":"\ud83d\udc24","CIRCLEBLACK":"\u26ab","CIRCLEBLUE":"\ud83d\udd35","CIRCLERED":"\ud83d\udd34","CIRCLEWHITE":"\u26aa","CIRCUS":"\ud83c\udfaa","CLAPPER":"\ud83c\udfac","CLAPPING":"\ud83d\udc4f","CLIP":"\ud83d\udcce","CLIPBOARD":"\ud83d\udccb","CLOUD":"\ud83c\udf28","CLOVER":"\ud83c\udf40","CLOWN":"\ud83e\udd21","COLDSWEAT":"\ud83d\ude13","COLDSWEAT2":"\ud83d\ude30","COMPRESS":"\ud83d\udddc","CONFOUNDED":"\ud83d\ude16","CONFUSED":"\ud83d\ude15","CONSTRUCTION":"\ud83d\udea7","CONTROL":"\ud83c\udf9b","COOKIE":"\ud83c\udf6a","COOKING":"\ud83c\udf73","COOL":"\ud83d\ude0e","COOLBOX":"\ud83c\udd92","COPYRIGHT":"\u00a9","CRANE":"\ud83c\udfd7","CRAYON":"\ud83d\udd8d","CREDITCARD":"\ud83d\udcb3","CROSS":"\u2716","CROSSBOX:":"\u274e","CRY":"\ud83d\ude22","CRYCAT":"\ud83d\ude3f","CRYSTALBALL":"\ud83d\udd2e","CUSTOMS":"\ud83d\udec3","DELICIOUS":"\ud83d\ude0b","DERELICT":"\ud83c\udfda","DESKTOP":"\ud83d\udda5","DIAMONDLB":"\ud83d\udd37","DIAMONDLO":"\ud83d\udd36","DIAMONDSB":"\ud83d\udd39","DIAMONDSO":"\ud83d\udd38","DICE":"\ud83c\udfb2","DISAPPOINTED":"\ud83d\ude1e","CRY2":"\ud83d\ude25","DIVISION":"\u2797","DIZZY":"\ud83d\ude35","DOLLAR":"\ud83d\udcb5","DOLLAR2":"\ud83d\udcb2","DOWNARROW":"\u2b07","DVD":"\ud83d\udcc0","EJECT":"\u23cf","ELEPHANT":"\ud83d\udc18","EMAIL":"\ud83d\udce7","ENVELOPE":"\ud83d\udce8","ENVELOPE2":"\u2709","ENVELOPE_DOWN":"\ud83d\udce9","EURO":"\ud83d\udcb6","EVIL":"\ud83d\ude08","EXPRESSIONLESS":"\ud83d\ude11","EYES":"\ud83d\udc40","FACTORY":"\ud83c\udfed","FAX":"\ud83d\udce0","FEARFUL":"\ud83d\ude28","FILEBOX":"\ud83d\uddc3","FILECABINET":"\ud83d\uddc4","FIRE":"\ud83d\udd25","FIREENGINE":"\ud83d\ude92","FIST":"\ud83d\udc4a","FLOWER":"\ud83c\udf37","FLOWER2":"\ud83c\udf38","FLUSHED":"\ud83d\ude33","FOLDER":"\ud83d\udcc1","FOLDER2":"\ud83d\udcc2","FREE":"\ud83c\udd93","FROG":"\ud83d\udc38","FROWN":"\ud83d\ude41","GEAR":"\u2699","GLOBE":"\ud83c\udf0d","GLOWINGSTAR":"\ud83c\udf1f","GOOD:":"\ud83d\udc4d","GRIMACING":"\ud83d\ude2c","GRIN":"\ud83d\ude00","GRINNINGCAT":"\ud83d\ude38","HALO":"\ud83d\ude07","HAMMER":"\ud83d\udd28","HAMSTER":"\ud83d\udc39","HAND":"\u270b","HANDDOWN":"\ud83d\udc47","HANDLEFT":"\ud83d\udc48","HANDRIGHT":"\ud83d\udc49","HANDUP":"\ud83d\udc46","HATCHING":"\ud83d\udc23","HAZARD":"\u2623","HEADPHONE":"\ud83c\udfa7","HEARNOEVIL":"\ud83d\ude49","HEARTBLUE":"\ud83d\udc99","HEARTEYES":"\ud83d\ude0d","HEARTGREEN":"\ud83d\udc9a","HEARTYELLOW":"\ud83d\udc9b","HELICOPTER":"\ud83d\ude81","HERB":"\ud83c\udf3f","HIGH_BRIGHTNESS":"\ud83d\udd06","HIGHVOLTAGE":"\u26a1","HIT":"\ud83c\udfaf","HONEY":"\ud83c\udf6f","HOT":"\ud83c\udf36","HOURGLASS":"\u23f3","HOUSE":"\ud83c\udfe0","HUGGINGFACE":"\ud83e\udd17","HUNDRED":"\ud83d\udcaf","HUSHED":"\ud83d\ude2f","ID":"\ud83c\udd94","INBOX":"\ud83d\udce5","INDEX":"\ud83d\uddc2","JOY":"\ud83d\ude02","KEY":"\ud83d\udd11","KISS":"\ud83d\ude18","KISS2":"\ud83d\ude17","KISS3":"\ud83d\ude19","KISS4":"\ud83d\ude1a","KISSINGCAT":"\ud83d\ude3d","KNIFE":"\ud83d\udd2a","LABEL":"\ud83c\udff7","LADYBIRD":"\ud83d\udc1e","LANDING":"\ud83d\udeec","LAPTOP":"\ud83d\udcbb","LEFTARROW":"\u2b05","LEMON":"\ud83c\udf4b","LIGHTNINGCLOUD":"\ud83c\udf29","LINK":"\ud83d\udd17","LITTER":"\ud83d\udeae","LOCK":"\ud83d\udd12","LOLLIPOP":"\ud83c\udf6d","LOUDSPEAKER":"\ud83d\udce2","LOW_BRIGHTNESS":"\ud83d\udd05","MAD":"\ud83d\ude1c","MAGNIFYING_GLASS":"\ud83d\udd0d","MASK":"\ud83d\ude37","MEDAL":"\ud83c\udf96","MEMO":"\ud83d\udcdd","MIC":"\ud83c\udfa4","MICROSCOPE":"\ud83d\udd2c","MINUS":"\u2796","MOBILE":"\ud83d\udcf1","MONEY":"\ud83d\udcb0","MONEYMOUTH":"\ud83e\udd11","MONKEY":"\ud83d\udc35","MOUSE":"\ud83d\udc2d","MOUSE2":"\ud83d\udc01","MOUTHLESS":"\ud83d\ude36","MOVIE":"\ud83c\udfa5","MUGS":"\ud83c\udf7b","NERD":"\ud83e\udd13","NEUTRAL":"\ud83d\ude10","NEW":"\ud83c\udd95","NOENTRY":"\ud83d\udeab","NOTEBOOK":"\ud83d\udcd4","NOTEPAD":"\ud83d\uddd2","NUTANDBOLT":"\ud83d\udd29","O":"\u2b55","OFFICE":"\ud83c\udfe2","OK":"\ud83c\udd97","OKHAND":"\ud83d\udc4c","OLDKEY":"\ud83d\udddd","OPENLOCK":"\ud83d\udd13","OPENMOUTH":"\ud83d\ude2e","OUTBOX":"\ud83d\udce4","PACKAGE":"\ud83d\udce6","PAGE":"\ud83d\udcc4","PAINTBRUSH":"\ud83d\udd8c","PALETTE":"\ud83c\udfa8","PANDA":"\ud83d\udc3c","PASSPORT":"\ud83d\udec2","PAWS":"\ud83d\udc3e","PEN":"\ud83d\udd8a","PEN2":"\ud83d\udd8b","PENSIVE":"\ud83d\ude14","PERFORMING":"\ud83c\udfad","PHONE":"\ud83d\udcde","PILL":"\ud83d\udc8a","PING":"\u2757","PLATE":"\ud83c\udf7d","PLUG":"\ud83d\udd0c","PLUS":"\u2795","POLICE":"\ud83d\ude93","POLICELIGHT":"\ud83d\udea8","POSTOFFICE":"\ud83c\udfe4","POUND":"\ud83d\udcb7","POUTING":"\ud83d\ude21","POUTINGCAT":"\ud83d\ude3e","PRESENT":"\ud83c\udf81","PRINTER":"\ud83d\udda8","PROJECTOR":"\ud83d\udcfd","PUSHPIN":"\ud83d\udccc","QUESTION":"\u2753","RABBIT":"\ud83d\udc30","RADIOACTIVE":"\u2622","RADIOBUTTON":"\ud83d\udd18","RAINCLOUD":"\ud83c\udf27","RAT":"\ud83d\udc00","RECYCLE":"\u267b","REGISTERED":"\u00ae","RELIEVED":"\ud83d\ude0c","ROBOT":"\ud83e\udd16","ROCKET":"\ud83d\ude80","ROLLING":"\ud83d\ude44","ROOSTER":"\ud83d\udc13","RULER":"\ud83d\udccf","SATELLITE":"\ud83d\udef0","SAVE":"\ud83d\udcbe","SCHOOL":"\ud83c\udfeb","SCISSORS":"\u2702","SCREAMING":"\ud83d\ude31","SCROLL":"\ud83d\udcdc","SEAT":"\ud83d\udcba","SEEDLING":"\ud83c\udf31","SEENOEVIL":"\ud83d\ude48","SHIELD":"\ud83d\udee1","SHIP":"\ud83d\udea2","SHOCKED":"\ud83d\ude32","SHOWER":"\ud83d\udebf","SLEEPING":"\ud83d\ude34","SLEEPY":"\ud83d\ude2a","SLIDER":"\ud83c\udf9a","SLOT":"\ud83c\udfb0","SMILE":"\ud83d\ude42","SMILING":"\ud83d\ude03","SMILINGCLOSEDEYES":"\ud83d\ude06","SMILINGEYES":"\ud83d\ude04","SMILINGSWEAT":"\ud83d\ude05","SMIRK":"\ud83d\ude0f","SNAIL":"\ud83d\udc0c","SNAKE":"\ud83d\udc0d","SOCCER":"\u26bd","SOS":"\ud83c\udd98","SPEAKER":"\ud83d\udd08","SPEAKEROFF":"\ud83d\udd07","SPEAKNOEVIL":"\ud83d\ude4a","SPIDER":"\ud83d\udd77","SPIDERWEB":"\ud83d\udd78","STAR":"\u2b50","STOP":"\u26d4","STOPWATCH":"\u23f1","SULK":"\ud83d\ude26","SUNFLOWER":"\ud83c\udf3b","SUNGLASSES":"\ud83d\udd76","SYRINGE":"\ud83d\udc89","TAKEOFF":"\ud83d\udeeb","TAXI":"\ud83d\ude95","TELESCOPE":"\ud83d\udd2d","TEMPORATURE":"\ud83e\udd12","TENNIS":"\ud83c\udfbe","THERMOMETER":"\ud83c\udf21","THINKING":"\ud83e\udd14","THUNDERCLOUD":"\u26c8","TICKBOX":"\u2705","TICKET":"\ud83c\udf9f","TIRED":"\ud83d\ude2b","TOILET":"\ud83d\udebd","TOMATO":"\ud83c\udf45","TONGUE":"\ud83d\ude1b","TOOLS":"\ud83d\udee0","TORCH":"\ud83d\udd26","TORNADO":"\ud83c\udf2a","TOUNG2":"\ud83d\ude1d","TRADEMARK":"\u2122","TRAFFICLIGHT":"\ud83d\udea6","TRASH":"\ud83d\uddd1","TREE":"\ud83c\udf32","TRIANGLE_LEFT":"\u25c0","TRIANGLE_RIGHT":"\u25b6","TRIANGLEDOWN":"\ud83d\udd3b","TRIANGLEUP":"\ud83d\udd3a","TRIANGULARFLAG":"\ud83d\udea9","TROPHY":"\ud83c\udfc6","TRUCK":"\ud83d\ude9a","TRUMPET":"\ud83c\udfba","TURKEY":"\ud83e\udd83","TURTLE":"\ud83d\udc22","UMBRELLA":"\u26f1","UNAMUSED":"\ud83d\ude12","UPARROW":"\u2b06","UPSIDEDOWN":"\ud83d\ude43","WARNING":"\u26a0","WATCH":"\u231a","WAVING":"\ud83d\udc4b","WEARY":"\ud83d\ude29","WEARYCAT":"\ud83d\ude40","WHITEFLAG":"\ud83c\udff3","WINEGLASS":"\ud83c\udf77","WINK":"\ud83d\ude09","WORRIED":"\ud83d\ude1f","WRENCH":"\ud83d\udd27","X":"\u274c","YEN":"\ud83d\udcb4","ZIPPERFACE":"\ud83e\udd10","UNDEFINED":"","":""};statuses={F:'FATAL',B:'BUG',C:'CRITICAL',E:'ERROR',W:'WARNING',I:'INFO',IM:'IMPORTANT',D:'DEBUG',L:'LOG',CO:'CONSTANT',FU:'FUNCTION',R:'RETURN',V:'VARIABLE',S:'STACK',RE:'RESULT',ST:'STOPPER',TI:'TIMER',T:'TRACE'};LOG_LEVEL={NONE:7,OFF:7,FATAL:6,ERROR:5,WARN:4,INFO:3,UNDEFINED:2,'':2,DEFAULT:2,DEBUG:2,TRACE:1,ON:0,ALL:0,};LOG_STATUS={OFF:LOG_LEVEL.OFF,NONE:LOG_LEVEL.OFF,NO:LOG_LEVEL.OFF,NOPE:LOG_LEVEL.OFF,FALSE:LOG_LEVEL.OFF,FATAL:LOG_LEVEL.FATAL,BUG:LOG_LEVEL.ERROR,CRITICAL:LOG_LEVEL.ERROR,ERROR:LOG_LEVEL.ERROR,WARNING:LOG_LEVEL.WARN,INFO:LOG_LEVEL.INFO,IMPORTANT:LOG_LEVEL.INFO,DEBUG:LOG_LEVEL.DEBUG,LOG:LOG_LEVEL.DEBUG,STACK:LOG_LEVEL.DEBUG,CONSTANT:LOG_LEVEL.DEBUG,FUNCTION:LOG_LEVEL.DEBUG,VARIABLE:LOG_LEVEL.DEBUG,RETURN:LOG_LEVEL.DEBUG,RESULT:LOG_LEVEL.TRACE,STOPPER:LOG_LEVEL.TRACE,TIMER:LOG_LEVEL.TRACE,TRACE:LOG_LEVEL.TRACE,ALL:LOG_LEVEL.ALL,YES:LOG_LEVEL.ALL,YEP:LOG_LEVEL.ALL,TRUE:LOG_LEVEL.ALL};var logFile,logFolder;var LOG=function(message,status,icon){if(LOG.level!==LOG_LEVEL.OFF&&(LOG.write||LOG.store)&&LOG.arguments.length)return LOG.addMessage(message,status,icon);};LOG.logDecodeLevel=function(level){if(level==~~level)return Math.abs(level);var lev;level+='';level=level.toUpperCase();if(level in statuses){level=statuses[level];}lev=LOG_LEVEL[level];if(lev!==undefined)return lev;lev=LOG_STATUS[level];if(lev!==undefined)return lev;return LOG_LEVEL.DEFAULT;};LOG.write=write;LOG.store=store;LOG.level=LOG.logDecodeLevel(level);LOG.status=defaultStatus;LOG.addMessage=function(message,status,icon){var date=new Date(),count,bool,logStatus;if(status&&status.constructor.name==='String'){status=status.toUpperCase();status=statuses[status]||status;}else status=LOG.status;logStatus=LOG_STATUS[status]||LOG_STATUS.ALL;if(logStatus999)?'['+LOG.count+'] ':(' ['+LOG.count+'] ').slice(-7);message=count+status+icon+(message instanceof Object?message.toSource():message)+date;if(LOG.store){stack.push(message);}if(LOG.write){bool=file&&file.writable&&logFile.writeln(message);if(!bool){file.writable=true;LOG.setFile(logFile);logFile.writeln(message);}}LOG.count++;return true;};var logNewFile=function(file,isCookie,overwrite){file.encoding='UTF-8';file.lineFeed=($.os[0]=='M')?'Macintosh':' Windows';if(isCookie)return file.open(overwrite?'w':'e')&&file;file.writable=LOG.write;logFile=file;logFolder=file.parent;if(continuing){LOG.count=LOG.setCount(file);}return(!LOG.write&&file||(file.open('a')&&file));};LOG.setFile=function(file,isCookie,overwrite){var bool,folder,fileName,suffix,newFileName,f,d,safeFileName;d=new Date();f=$.stack.split("\n")[0].replace(/^\[\(?/,'').replace(/\)?\]$/,'');if(f==~~f){f=$.fileName.replace(/[^\/]+\//g,'');}safeFileName=File.encode((isCookie?'/COOKIE_':'/LOG_')+f.replace(/^\//,'')+'_'+(1900+d.getYear())+(''+d).replace(/...(...)(..).+/,'_$1_$2')+(isCookie?'.txt':'.log'));if(file&&file.constructor.name=='String'){file=(file.match('/'))?new File(file):new File((logFolder||Folder.temp)+'/'+file);}if(file instanceof File){folder=file.parent;bool=folder.exists||folder.create();if(!bool)folder=Folder.temp;fileName=File.decode(file.name);suffix=fileName.match(/\.[^.]+$/);suffix=suffix?suffix[0]:'';fileName='/'+fileName;newFileName=fileName.replace(/\.[^.]+$/,'')+'_'+(+(new Date())+suffix);f=logNewFile(file,isCookie,overwrite);if(f)return f;f=logNewFile(new File(folder+newFileName),isCookie,overwrite);if(f)return f;f=logNewFile(new File(folder+safeFileName),isCookie,overwrite);if(f)return f;if(folder!=Folder.temp){f=logNewFile(new File(Folder.temp+fileName),isCookie,overwrite);if(f)return f;f=logNewFile(new File(Folder.temp+safeFileName),isCookie,overwrite);return f||new File(Folder.temp+safeFileName);}}return LOG.setFile(((logFile&&!isCookie)?new File(logFile):new File(Folder.temp+safeFileName)),isCookie,overwrite );};LOG.setCount=function(file){if(~~file===file){LOG.count=file;return LOG.count;}if(file===undefined){file=logFile;}if(file&&file.constructor===String){file=new File(file);}var logNumbers,contents;if(!file.length||!file.exists){LOG.count=1;return 1;}file.open('r');file.encoding='utf-8';file.seek(10000,2);contents='\n'+file.read();logNumbers=contents.match(/\n{0,3}\[\d+\] \[\w+\]+/g);if(logNumbers){logNumbers=+logNumbers[logNumbers.length-1].match(/\d+/)+1;file.close();LOG.count=logNumbers;return logNumbers;}if(file.length<10001){file.close();LOG.count=1;return 1;}file.seek(10000000,2);contents='\n'+file.read();logNumbers=contents.match(/\n{0,3}\[\d+\] \[\w+\]+/g);if(logNumbers){logNumbers=+logNumbers[logNumbers.length-1].match(/\d+/)+1;file.close();LOG.count=logNumbers;return logNumbers;}file.close();LOG.count=1;return 1;};LOG.setLevel=function(level){LOG.level=LOG.logDecodeLevel(level);return LOG.level;};LOG.setStatus=function(status){status=(''+status).toUpperCase();LOG.status=statuses[status]||status;return LOG.status;};LOG.cookie=function(file,level,overwrite,setLevel){var log,cookie;if(!file){file={file:file};}if(file&&(file.constructor===String||file.constructor===File)){file={file:file};}log=file;if(log.level===undefined){log.level=(level!==undefined)?level:'NONE';}if(log.overwrite===undefined){log.overwrite=(overwrite!==undefined)?overwrite:false;}if(log.setLevel===undefined){log.setLevel=(setLevel!==undefined)?setLevel:true;}setLevel=log.setLevel;overwrite=log.overwrite;level=log.level;file=log.file;file=LOG.setFile(file,true,overwrite);if(overwrite){file.write(level);}else{cookie=file.read();if(cookie.length){level=cookie;}else{file.write(level);}}file.close();if(setLevel){LOG.setLevel(level);}return{path:file,level:level};};LOG.args=function(args,funct,line){if(LOG.level>LOG_STATUS.FUNCTION)return;if(!(args&&(''+args.constructor).replace(/\s+/g,'')==='functionObject(){[nativecode]}'))return;if(!LOG.args.STRIP_COMMENTS){LOG.args.STRIP_COMMENTS=/((\/.*$)|(\/\*[\s\S]*?\*\/))/mg;}if(!LOG.args.ARGUMENT_NAMES){LOG.args.ARGUMENT_NAMES=/([^\s,]+)/g;}if(!LOG.args.OUTER_BRACKETS){LOG.args.OUTER_BRACKETS=/^\((.+)?\)$/;}if(!LOG.args.NEW_SOMETHING){LOG.args.NEW_SOMETHING=/^new \w+\((.+)?\)$/;}var functionString,argumentNames,stackInfo,report,functionName,arg,argsL,n,argName,argValue,argsTotal;if(funct===~~funct){line=funct;}if(!(funct instanceof Function)){funct=args.callee;}if(!(funct instanceof Function))return;functionName=funct.name;functionString=(''+funct).replace(LOG.args.STRIP_COMMENTS,'');argumentNames=functionString.slice(functionString.indexOf('(')+1,functionString.indexOf(')')).match(LOG.args.ARGUMENT_NAMES);argumentNames=argumentNames||[];report=[];report.push('--------------');report.push('Function Data:');report.push('--------------');report.push('Function Name:'+functionName);argsL=args.length;stackInfo=$.stack.split(/[\n\r]/);stackInfo.pop();stackInfo=stackInfo.join('\n ');report.push('Call stack:'+stackInfo);if(line){report.push('Function Line around:'+line);}report.push('Arguments Provided:'+argsL);report.push('Named Arguments:'+argumentNames.length);if(argumentNames.length){report.push('Arguments Names:'+argumentNames.join(','));}if(argsL){report.push('----------------');report.push('Argument Values:');report.push('----------------');}argsTotal=Math.max(argsL,argumentNames.length);for(n=0;n=argsL){argValue='NO VALUE PROVIDED';}else if(arg===undefined){argValue='undefined';}else if(arg===null){argValue='null';}else{argValue=arg.toSource().replace(LOG.args.OUTER_BRACKETS,'$1').replace(LOG.args.NEW_SOMETHING,'$1');}report.push((argName?argName:'arguments['+n+']')+':'+argValue);}report.push('');report=report.join('\n ');LOG(report,'f');return report;};LOG.stack=function(reverse){var st=$.stack.split('\n');st.pop();st.pop();if(reverse){st.reverse();}return LOG(st.join('\n '),'s');};LOG.values=function(values){var n,value,map=[];if(!(values instanceof Object||values instanceof Array)){return;}if(!LOG.values.OUTER_BRACKETS){LOG.values.OUTER_BRACKETS=/^\((.+)?\)$/;}if(!LOG.values.NEW_SOMETHING){LOG.values.NEW_SOMETHING=/^new \w+\((.+)?\)$/;}for(n in values){try{value=values[n];if(value===undefined){value='undefined';}else if(value===null){value='null';}else{value=value.toSource().replace(LOG.values.OUTER_BRACKETS,'$1').replace(LOG.values.NEW_SOMETHING,'$1');}}catch(e){value='\uD83D\uDEAB '+e;}map.push(n+':'+value);}if(map.length){map=map.join('\n ')+'\n ';return LOG(map,'v');}};LOG.reset=function(all){stack.length=0;LOG.count=1;if(all!==false){if(logFile instanceof File){logFile.close();}logFile=LOG.store=LOG.writeToFile=undefined;LOG.write=true;logFolder=Folder.temp;logTime=new Date();logPoint='After Log Reset';}};LOG.stopper=function(message){var newLogTime,t,m,newLogPoint;newLogTime=new Date();newLogPoint=(LOG.count!==undefined)?'LOG#'+LOG.count:'BEFORE LOG#1';LOG.time=t=newLogTime-logTime;if(message===false){return;}message=message||'Stopper start point';t=LOG.prettyTime(t);m=message+'\n '+'From '+logPoint+' to '+newLogPoint+' took '+t+' Starting '+logTime+' '+logTime.getMilliseconds()+'ms'+' Ending '+newLogTime+' '+newLogTime.getMilliseconds()+'ms';LOG(m,'st');logPoint=newLogPoint;logTime=newLogTime;return m;};LOG.start=function(message){var t=new Date();times.push([t,(message!==undefined)?message+'':'']);};LOG.stop=function(message){if(!times.length)return;message=(message)?message+' ':'';var nt,startLog,ot,om,td,m;nt=new Date();startLog=times.pop();ot=startLog[0];om=startLog[1];td=nt-ot;if(om.length){om+=' ';}m=om+'STARTED ['+ot+' '+ot.getMilliseconds()+'ms]\n '+message+'FINISHED ['+nt+' '+nt.getMilliseconds()+'ms]\n TOTAL TIME ['+LOG.prettyTime(td)+']';LOG(m,'ti');return m;};LOG.prettyTime=function(t){var h,m,s,ms;h=Math.floor(t / 3600000);m=Math.floor((t % 3600000)/ 60000);s=Math.floor((t % 60000)/ 1000);ms=t % 1000;t=(!t)?'<1ms':((h)?h+' hours ':'')+((m)?m+' minutes ':'')+((s)?s+' seconds ':'')+((ms&&(h||m||s))?'&':'')+((ms)?ms+'ms':'');return t;};LOG.get=function(){if(!stack.length)return 'THE LOG IS NOT SET TO STORE';var a=fetchLogLines(arguments);return a?'\n'+a.join('\n'):'NO LOGS AVAILABLE';};var fetchLogLines=function(){var args=arguments[0];if(!args.length)return stack;var c,n,l,a=[],ln,start,end,j,sl;l=args.length;sl=stack.length-1;n=0;for(c=0;cln)?sl+ln+1:ln-1;if(ln>=0&&ln<=sl)a[n++]=stack[ln];}else if(ln instanceof Array&&ln.length===2){start=ln[0];end=ln[1];if(!(~~start===start&&~~end===end))continue;start=(0>start)?sl+start+1:start-1;end=(0>end)?sl+end+1:end-1;start=Math.max(Math.min(sl,start),0);end=Math.min(Math.max(end,0),sl);if(start<=end)for(j=start;j<=end;j++)a[n++]=stack[j];else for(j=start;j>=end;j--)a[n++]=stack[j];}}return(n)?a:false;};LOG.file=function(){return logFile;};LOG.openFolder=function(){if(logFolder)return logFolder.execute();};LOG.show=LOG.execute=function(){if(logFile)return logFile.execute();};LOG.close=function(){if(logFile)return logFile.close();};LOG.setFile(file);if(!$.summary.difference){$.summary.difference=function(){return $.summary().replace(/ *([0-9]+)([^ ]+)(\n?)/g,$.summary.updateSnapshot );};}if(!$.summary.updateSnapshot){$.summary.updateSnapshot=function(full,count,name,lf){var snapshot=$.summary.snapshot;count=Number(count);var prev=snapshot[name]?snapshot[name]:0;snapshot[name]=count;var diff=count-prev;if(diff===0)return "";return " ".substring(String(diff).length)+diff+" "+name+lf;};}if(!$.summary.snapshot){$.summary.snapshot=[];$.summary.difference();}$.gc();$.gc();$.summary.difference();LOG.sumDiff=function(message){$.gc();$.gc();var diff=$.summary.difference();if(diff.length<8){diff=' - NONE -';}if(message===undefined){message='';}message+=diff;return LOG('$.summary.difference():'+message,'v');};return LOG;}; + +var log = new LogFactory('myLog.log'); // =>; creates the new log factory - put full path where + +function getEnv(variable){ + return $.getenv(variable); +} + +function fileOpen(path){ + return app.open(new File(path)); +} + +function getLayerTypeWithName(layerName) { + var type = 'NA'; + var nameParts = layerName.split('_'); + var namePrefix = nameParts[0]; + namePrefix = namePrefix.toLowerCase(); + switch (namePrefix) { + case 'guide': + case 'tl': + case 'tr': + case 'bl': + case 'br': + type = 'GUIDE'; + break; + case 'fg': + type = 'FG'; + break; + case 'bg': + type = 'BG'; + break; + case 'obj': + default: + type = 'OBJ'; + break; + } + + return type; +} + +function getLayers() { + /** + * Get json representation of list of layers. + * Much faster this way than in DOM traversal (2s vs 45s on same file) + * + * Format of single layer info: + * id : number + * name: string + * group: boolean - true if layer is a group + * parents:array - list of ids of parent groups, useful for selection + * all children layers from parent layerSet (eg. group) + * type: string - type of layer guessed from its name + * visible:boolean - true if visible + **/ + if (documents.length == 0){ + return '[]'; + } + var ref1 = new ActionReference(); + ref1.putEnumerated(charIDToTypeID('Dcmn'), charIDToTypeID('Ordn'), + charIDToTypeID('Trgt')); + var count = executeActionGet(ref1).getInteger(charIDToTypeID('NmbL')); + + // get all layer names + var layers = []; + var layer = {}; + + var parents = []; + for (var i = count; i >= 1; i--) { + var layer = {}; + var ref2 = new ActionReference(); + ref2.putIndex(charIDToTypeID('Lyr '), i); + + var desc = executeActionGet(ref2); // Access layer index #i + var layerSection = typeIDToStringID(desc.getEnumerationValue( + stringIDToTypeID('layerSection'))); + + layer.id = desc.getInteger(stringIDToTypeID("layerID")); + layer.name = desc.getString(stringIDToTypeID("name")); + layer.color_code = typeIDToStringID(desc.getEnumerationValue(stringIDToTypeID('color'))); + layer.group = false; + layer.parents = parents.slice(); + layer.type = getLayerTypeWithName(layer.name); + layer.visible = desc.getBoolean(stringIDToTypeID("visible")); + //log(" name: " + layer.name + " groupId " + layer.groupId + + //" group " + layer.group); + if (layerSection == 'layerSectionStart') { // Group start and end + parents.push(layer.id); + layer.group = true; + } + if (layerSection == 'layerSectionEnd') { + parents.pop(); + continue; + } + layers.push(JSON.stringify(layer)); + } + try{ + var bck = activeDocument.backgroundLayer; + layer.id = bck.id; + layer.name = bck.name; + layer.group = false; + layer.parents = []; + layer.type = 'background'; + layer.visible = bck.visible; + layers.push(JSON.stringify(layer)); + }catch(e){ + // do nothing, no background layer + }; + //log("layers " + layers); + return '[' + layers + ']'; +} + +function setVisible(layer_id, visibility){ + /** + * Sets particular 'layer_id' to 'visibility' if true > show + **/ + var desc = new ActionDescriptor(); + var ref = new ActionReference(); + ref.putIdentifier(stringIDToTypeID("layer"), layer_id); + desc.putReference(stringIDToTypeID("null"), ref); + + executeAction(visibility?stringIDToTypeID("show"):stringIDToTypeID("hide"), + desc, DialogModes.NO); + +} + +function getHeadline(){ + /** + * Returns headline of current document with metadata + * + **/ + if (documents.length == 0){ + return ''; + } + var headline = app.activeDocument.info.headline; + + return headline; +} + +function isSaved(){ + return app.activeDocument.saved; +} + +function save(){ + /** Saves active document **/ + return app.activeDocument.save(); +} + +function saveAs(output_path, ext, as_copy){ + /** Exports scene to various formats + * + * Currently implemented: 'jpg', 'png', 'psd' + * + * output_path - escaped file path on local system + * ext - extension for export + * as_copy - create copy, do not overwrite + * + * */ + var saveName = output_path; + var saveOptions; + if (ext == 'jpg'){ + saveOptions = new JPEGSaveOptions(); + saveOptions.quality = 12; + saveOptions.embedColorProfile = true; + saveOptions.formatOptions = FormatOptions.PROGRESSIVE; + if(saveOptions.formatOptions == FormatOptions.PROGRESSIVE){ + saveOptions.scans = 5}; + saveOptions.matte = MatteType.NONE; + } + if (ext == 'png'){ + saveOptions = new PNGSaveOptions(); + saveOptions.interlaced = true; + saveOptions.transparency = true; + } + if (ext == 'psd'){ + saveOptions = null; + return app.activeDocument.saveAs(new File(saveName)); + } + if (ext == 'psb'){ + return savePSB(output_path); + } + + return app.activeDocument.saveAs(new File(saveName), saveOptions, as_copy); + +} + +function getActiveDocumentName(){ + /** + * Returns file name of active document + * */ + if (documents.length == 0){ + return null; + } + return app.activeDocument.name; +} + +function getActiveDocumentFullName(){ + /** + * Returns file name of active document with file path. + * activeDocument.fullName returns path in URI (eg /c/.. insted of c:/) + * */ + if (documents.length == 0){ + return null; + } + var f = new File(app.activeDocument.fullName); + var path = f.fsName; + f.close(); + return path; +} + +function imprint(payload){ + /** + * Sets headline content of current document with metadata. Stores + * information about assets created through Avalon. + * Content accessible in PS through File > File Info + * + **/ + app.activeDocument.info.headline = payload; +} + +function getSelectedLayers(doc) { + /** + * Returns json representation of currently selected layers. + * Works in three steps - 1) creates new group with selected layers + * 2) traverses this group + * 3) deletes newly created group, not neede + * Bit weird, but Adobe.. + **/ + if (doc == null){ + doc = app.activeDocument; + } + + var selLayers = []; + _grp = groupSelectedLayers(doc); + + var group = doc.activeLayer; + var layers = group.layers; + + // // group is fake at this point + // var itself_name = ''; + // if (layers){ + // itself_name = layers[0].name; + // } + + + for (var i = 0; i < layers.length; i++) { + var layer = {}; + layer.id = layers[i].id; + layer.name = layers[i].name; + long_names =_get_parents_names(group.parent, layers[i].name); + var t = layers[i].kind; + if ((typeof t !== 'undefined') && + (layers[i].kind.toString() == 'LayerKind.NORMAL')){ + layer.group = false; + }else{ + layer.group = true; + } + layer.long_name = long_names; + + selLayers.push(layer); + } + + _undo(); + + return JSON.stringify(selLayers); +}; + +function selectLayers(selectedLayers){ + /** + * Selects layers from list of ids + **/ + selectedLayers = JSON.parse(selectedLayers); + var layers = new Array(); + var id54 = charIDToTypeID( "slct" ); + var desc12 = new ActionDescriptor(); + var id55 = charIDToTypeID( "null" ); + var ref9 = new ActionReference(); + + var existing_layers = JSON.parse(getLayers()); + var existing_ids = []; + for (var y = 0; y < existing_layers.length; y++){ + existing_ids.push(existing_layers[y]["id"]); + } + for (var i = 0; i < selectedLayers.length; i++) { + // a check to see if the id stil exists + var id = selectedLayers[i]; + if(existing_ids.toString().indexOf(id)>=0){ + layers[i] = charIDToTypeID( "Lyr " ); + ref9.putIdentifier(layers[i], id); + } + } + desc12.putReference( id55, ref9 ); + var id58 = charIDToTypeID( "MkVs" ); + desc12.putBoolean( id58, false ); + executeAction( id54, desc12, DialogModes.NO ); +} + +function groupSelectedLayers(doc, name) { + /** + * Groups selected layers into new group. + * Returns json representation of Layer for server to consume + * + * Args: + * doc(activeDocument) + * name (str): new name of created group + **/ + if (doc == null){ + doc = app.activeDocument; + } + + var desc = new ActionDescriptor(); + var ref = new ActionReference(); + ref.putClass( stringIDToTypeID('layerSection') ); + desc.putReference( charIDToTypeID('null'), ref ); + var lref = new ActionReference(); + lref.putEnumerated( charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), + charIDToTypeID('Trgt') ); + desc.putReference( charIDToTypeID('From'), lref); + executeAction( charIDToTypeID('Mk '), desc, DialogModes.NO ); + + var group = doc.activeLayer; + if (name){ + // Add special character to highlight group that will be published + group.name = name; + } + var layer = {}; + layer.id = group.id; + layer.name = name; // keep name clean + layer.group = true; + + layer.long_name = _get_parents_names(group, name); + + return JSON.stringify(layer); +}; + +function importSmartObject(path, name, link){ + /** + * Creates new layer with an image from 'path' + * + * path: absolute path to loaded file + * name: sets name of newly created laye + * + **/ + var desc1 = new ActionDescriptor(); + desc1.putPath( app.charIDToTypeID("null"), new File(path) ); + link = link || false; + if (link) { + desc1.putBoolean( app.charIDToTypeID('Lnkd'), true ); + } + + desc1.putEnumerated(app.charIDToTypeID("FTcs"), app.charIDToTypeID("QCSt"), + app.charIDToTypeID("Qcsa")); + var desc2 = new ActionDescriptor(); + desc2.putUnitDouble(app.charIDToTypeID("Hrzn"), + app.charIDToTypeID("#Pxl"), 0.0); + desc2.putUnitDouble(app.charIDToTypeID("Vrtc"), + app.charIDToTypeID("#Pxl"), 0.0); + + desc1.putObject(charIDToTypeID("Ofst"), charIDToTypeID("Ofst"), desc2); + executeAction(charIDToTypeID("Plc " ), desc1, DialogModes.NO); + + var docRef = app.activeDocument + var currentActivelayer = app.activeDocument.activeLayer; + if (name){ + currentActivelayer.name = name; + } + var layer = {} + layer.id = currentActivelayer.id; + layer.name = currentActivelayer.name; + return JSON.stringify(layer); +} + +function replaceSmartObjects(layer_id, path, name){ + /** + * Updates content of 'layer' with an image from 'path' + * + **/ + + var desc = new ActionDescriptor(); + var ref = new ActionReference(); + ref.putIdentifier(stringIDToTypeID("layer"), layer_id); + desc.putReference(stringIDToTypeID("null"), ref); + + desc.putPath(charIDToTypeID('null'), new File(path) ); + desc.putInteger(charIDToTypeID("PgNm"), 1); + + executeAction(stringIDToTypeID('placedLayerReplaceContents'), + desc, DialogModes.NO ); + var currentActivelayer = app.activeDocument.activeLayer; + if (name){ + currentActivelayer.name = name; + } +} + +function createGroup(name){ + /** + * Creates new group with a 'name' + * Because of asynchronous nature, only group.id is available + **/ + group = app.activeDocument.layerSets.add(); + // Add special character to highlight group that will be published + group.name = name; + + return group.id; // only id available at this time :| +} + +function deleteLayer(layer_id){ + /*** + * Deletes layer by its layer_id + * + * layer_id (int) + **/ + var d = new ActionDescriptor(); + var r = new ActionReference(); + + r.putIdentifier(stringIDToTypeID("layer"), layer_id); + d.putReference(stringIDToTypeID("null"), r); + executeAction(stringIDToTypeID("delete"), d, DialogModes.NO); +} + +function _undo() { + executeAction(charIDToTypeID("undo", undefined, DialogModes.NO)); +}; + +function savePSB(output_path){ + /*** + * Saves file as .psb to 'output_path' + * + * output_path (str) + **/ + var desc1 = new ActionDescriptor(); + var desc2 = new ActionDescriptor(); + desc2.putBoolean( stringIDToTypeID('maximizeCompatibility'), true ); + desc1.putObject( charIDToTypeID('As '), charIDToTypeID('Pht8'), desc2 ); + desc1.putPath( charIDToTypeID('In '), new File(output_path) ); + desc1.putBoolean( charIDToTypeID('LwCs'), true ); + executeAction( charIDToTypeID('save'), desc1, DialogModes.NO ); +} + +function close(){ + executeAction(stringIDToTypeID("quit"), undefined, DialogModes.NO ); +} + +function renameLayer(layer_id, new_name){ + /*** + * Renames 'layer_id' to 'new_name' + * + * Via Action (fast) + * + * Args: + * layer_id(int) + * new_name(str) + * + * output_path (str) + **/ + doc = app.activeDocument; + selectLayers('['+layer_id+']'); + + doc.activeLayer.name = new_name; +} + +function _get_parents_names(layer, itself_name){ + var long_names = [itself_name]; + while (layer.parent){ + if (layer.typename != "LayerSet"){ + break; + } + long_names.push(layer.name); + layer = layer.parent; + } + return long_names; +} + +// triggers when panel is opened, good for debugging +//log(getActiveDocumentName()); +// log.show(); +// var a = app.activeDocument.activeLayer; +// log(a); +//getSelectedLayers(); +// importSmartObject("c:/projects/test.jpg", "a aaNewLayer", true); +// log("dpc"); +// replaceSmartObjects(153, "â–¼Jungle_imageTest_001", "c:/projects/test_project_test_asset_TestTask_v001.png"); \ No newline at end of file diff --git a/openpype/hosts/photoshop/api/extension/host/json.js b/openpype/hosts/photoshop/api/extension/host/json.js new file mode 100644 index 00000000000..397349bbfdf --- /dev/null +++ b/openpype/hosts/photoshop/api/extension/host/json.js @@ -0,0 +1,530 @@ +// json2.js +// 2017-06-12 +// Public Domain. +// NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + +// USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO +// NOT CONTROL. + +// This file creates a global JSON object containing two methods: stringify +// and parse. This file provides the ES5 JSON capability to ES3 systems. +// If a project might run on IE8 or earlier, then this file should be included. +// This file does nothing on ES5 systems. + +// JSON.stringify(value, replacer, space) +// value any JavaScript value, usually an object or array. +// replacer an optional parameter that determines how object +// values are stringified for objects. It can be a +// function or an array of strings. +// space an optional parameter that specifies the indentation +// of nested structures. If it is omitted, the text will +// be packed without extra whitespace. If it is a number, +// it will specify the number of spaces to indent at each +// level. If it is a string (such as "\t" or " "), +// it contains the characters used to indent at each level. +// This method produces a JSON text from a JavaScript value. +// When an object value is found, if the object contains a toJSON +// method, its toJSON method will be called and the result will be +// stringified. A toJSON method does not serialize: it returns the +// value represented by the name/value pair that should be serialized, +// or undefined if nothing should be serialized. The toJSON method +// will be passed the key associated with the value, and this will be +// bound to the value. + +// For example, this would serialize Dates as ISO strings. + +// Date.prototype.toJSON = function (key) { +// function f(n) { +// // Format integers to have at least two digits. +// return (n < 10) +// ? "0" + n +// : n; +// } +// return this.getUTCFullYear() + "-" + +// f(this.getUTCMonth() + 1) + "-" + +// f(this.getUTCDate()) + "T" + +// f(this.getUTCHours()) + ":" + +// f(this.getUTCMinutes()) + ":" + +// f(this.getUTCSeconds()) + "Z"; +// }; + +// You can provide an optional replacer method. It will be passed the +// key and value of each member, with this bound to the containing +// object. The value that is returned from your method will be +// serialized. If your method returns undefined, then the member will +// be excluded from the serialization. + +// If the replacer parameter is an array of strings, then it will be +// used to select the members to be serialized. It filters the results +// such that only members with keys listed in the replacer array are +// stringified. + +// Values that do not have JSON representations, such as undefined or +// functions, will not be serialized. Such values in objects will be +// dropped; in arrays they will be replaced with null. You can use +// a replacer function to replace those with JSON values. + +// JSON.stringify(undefined) returns undefined. + +// The optional space parameter produces a stringification of the +// value that is filled with line breaks and indentation to make it +// easier to read. + +// If the space parameter is a non-empty string, then that string will +// be used for indentation. If the space parameter is a number, then +// the indentation will be that many spaces. + +// Example: + +// text = JSON.stringify(["e", {pluribus: "unum"}]); +// // text is '["e",{"pluribus":"unum"}]' + +// text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t"); +// // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + +// text = JSON.stringify([new Date()], function (key, value) { +// return this[key] instanceof Date +// ? "Date(" + this[key] + ")" +// : value; +// }); +// // text is '["Date(---current time---)"]' + +// JSON.parse(text, reviver) +// This method parses a JSON text to produce an object or array. +// It can throw a SyntaxError exception. + +// The optional reviver parameter is a function that can filter and +// transform the results. It receives each of the keys and values, +// and its return value is used instead of the original value. +// If it returns what it received, then the structure is not modified. +// If it returns undefined then the member is deleted. + +// Example: + +// // Parse the text. Values that look like ISO date strings will +// // be converted to Date objects. + +// myData = JSON.parse(text, function (key, value) { +// var a; +// if (typeof value === "string") { +// a = +// /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); +// if (a) { +// return new Date(Date.UTC( +// +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6] +// )); +// } +// return value; +// } +// }); + +// myData = JSON.parse( +// "[\"Date(09/09/2001)\"]", +// function (key, value) { +// var d; +// if ( +// typeof value === "string" +// && value.slice(0, 5) === "Date(" +// && value.slice(-1) === ")" +// ) { +// d = new Date(value.slice(5, -1)); +// if (d) { +// return d; +// } +// } +// return value; +// } +// ); + +// This is a reference implementation. You are free to copy, modify, or +// redistribute. + +/*jslint + eval, for, this +*/ + +/*property + JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (typeof JSON !== "object") { + JSON = {}; +} + +(function () { + "use strict"; + + var rx_one = /^[\],:{}\s]*$/; + var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; + var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; + var rx_four = /(?:^|:|,)(?:\s*\[)+/g; + var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + + function f(n) { + // Format integers to have at least two digits. + return (n < 10) + ? "0" + n + : n; + } + + function this_value() { + return this.valueOf(); + } + + if (typeof Date.prototype.toJSON !== "function") { + + Date.prototype.toJSON = function () { + + return isFinite(this.valueOf()) + ? ( + this.getUTCFullYear() + + "-" + + f(this.getUTCMonth() + 1) + + "-" + + f(this.getUTCDate()) + + "T" + + f(this.getUTCHours()) + + ":" + + f(this.getUTCMinutes()) + + ":" + + f(this.getUTCSeconds()) + + "Z" + ) + : null; + }; + + Boolean.prototype.toJSON = this_value; + Number.prototype.toJSON = this_value; + String.prototype.toJSON = this_value; + } + + var gap; + var indent; + var meta; + var rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + rx_escapable.lastIndex = 0; + return rx_escapable.test(string) + ? "\"" + string.replace(rx_escapable, function (a) { + var c = meta[a]; + return typeof c === "string" + ? c + : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); + }) + "\"" + : "\"" + string + "\""; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i; // The loop counter. + var k; // The member key. + var v; // The member value. + var length; + var mind = gap; + var partial; + var value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if ( + value + && typeof value === "object" + && typeof value.toJSON === "function" + ) { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === "function") { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case "string": + return quote(value); + + case "number": + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return (isFinite(value)) + ? String(value) + : "null"; + + case "boolean": + case "null": + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce "null". The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is "object", we might be dealing with an object or an array or +// null. + + case "object": + +// Due to a specification blunder in ECMAScript, typeof null is "object", +// so watch out for that case. + + if (!value) { + return "null"; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === "[object Array]") { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || "null"; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 + ? "[]" + : gap + ? ( + "[\n" + + gap + + partial.join(",\n" + gap) + + "\n" + + mind + + "]" + ) + : "[" + partial.join(",") + "]"; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === "object") { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === "string") { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + ( + (gap) + ? ": " + : ":" + ) + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + ( + (gap) + ? ": " + : ":" + ) + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 + ? "{}" + : gap + ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" + : "{" + partial.join(",") + "}"; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== "function") { + meta = { // table of character substitutions + "\b": "\\b", + "\t": "\\t", + "\n": "\\n", + "\f": "\\f", + "\r": "\\r", + "\"": "\\\"", + "\\": "\\\\" + }; + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ""; + indent = ""; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === "number") { + for (i = 0; i < space; i += 1) { + indent += " "; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === "string") { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== "function" && ( + typeof replacer !== "object" + || typeof replacer.length !== "number" + )) { + throw new Error("JSON.stringify"); + } + +// Make a fake root object containing our value under the key of "". +// Return the result of stringifying the value. + + return str("", {"": value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== "function") { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k; + var v; + var value = holder[key]; + if (value && typeof value === "object") { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + rx_dangerous.lastIndex = 0; + if (rx_dangerous.test(text)) { + text = text.replace(rx_dangerous, function (a) { + return ( + "\\u" + + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) + ); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with "()" and "new" +// because they can cause invocation, and "=" because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we +// replace all simple value tokens with "]" characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or "]" or +// "," or ":" or "{" or "}". If that is so, then the text is safe for eval. + + if ( + rx_one.test( + text + .replace(rx_two, "@") + .replace(rx_three, "]") + .replace(rx_four, "") + ) + ) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The "{" operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval("(" + text + ")"); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return (typeof reviver === "function") + ? walk({"": j}, "") + : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError("JSON.parse"); + }; + } +}()); \ No newline at end of file diff --git a/openpype/hosts/photoshop/api/extension/icons/avalon-logo-48.png b/openpype/hosts/photoshop/api/extension/icons/avalon-logo-48.png new file mode 100644 index 0000000000000000000000000000000000000000..33fe2a606bd1ac9d285eb0d6a90b9b14150ca3c4 GIT binary patch literal 1362 zcmV-Y1+DstP)5JpvKiH|A7SK4P~kT{6`M*9LrdQ!Ns9=$qj3(E_W@4gmP#=ht)#^0_psT^(5Px6qr0)mBB%5&-P}{Y*9ph@Pcn`!ete zwiE<#115v#ScdV2GBk!NTFTzWbF>Xip`p8%&KqcqI~Jb6tC``Vaf&07o~axnzSGF( z(ok|5&-4zgtV5rc$qke?7a8cU$D55m^%IcuOgXaxfTb~yegblyEaWJw%`Qe=-M%S@ zhOXSbt2KkcJv{&)s&PL6vC{g1Y-aKYBs(yc@x{whhk_0fK#=N=)Uup zs)>qe=dc=h3&3Gwr10?^8zc#g%1L4Xs{p!rj(uw=)9Szs&#`@sH{=+ zG+fz{pjE0VR%8l+hOX;W8`PbV32glOJ!~I2VXJkTz5Ufkuk(!F8z4>Ok_kkI+Kb}3)n06_ssJy4_*!y{BAe4)9jbBbSR!>UnLxyMT9bL9_?YdfL@K^^G6aZ)C$Qje z(NzKf2bZq2#ed1=gx1ZJQM{TNMk>CBw!wSvUjy@gS4qs1_a85GREVYsFz!+tU$`&M%7iR@HuBiw5bSa5S}|?)>G0PCUMb-Q{Pf zZt0{hEhroOCi1l=h%&q$mkBdG$MzLns~iea1>hEds{qcP5QbL){0`u*@Qfwke+13^ UGpuMiD*ylh07*qoM6N<$g1d2qT>t<8 literal 0 HcmV?d00001 diff --git a/openpype/hosts/photoshop/api/extension/index.html b/openpype/hosts/photoshop/api/extension/index.html new file mode 100644 index 00000000000..501e753c0b4 --- /dev/null +++ b/openpype/hosts/photoshop/api/extension/index.html @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py new file mode 100644 index 00000000000..36347b8ce0e --- /dev/null +++ b/openpype/hosts/photoshop/api/launch_logic.py @@ -0,0 +1,315 @@ +import os +import subprocess +import collections +import logging +import asyncio +import functools + +from wsrpc_aiohttp import ( + WebSocketRoute, + WebSocketAsync +) + +from Qt import QtCore + +from openpype.tools.utils import host_tools + +from avalon import api +from avalon.tools.webserver.app import WebServerTool + +from .ws_stub import PhotoshopServerStub + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + + +class ConnectionNotEstablishedYet(Exception): + pass + + +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: 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: + from openpype.api import Logger + + 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): + cls._main_thread_callbacks.append(callback) + + 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: + callback = cls._main_thread_callbacks.popleft() + callback() + + 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.""" + + partial_method = functools.partial(show_tool_by_name, + _tool_name) + + ProcessLauncher.execute_in_main_thread(partial_method) + + # Required return statement. + return "nothing" diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py new file mode 100644 index 00000000000..bc1fb36cf38 --- /dev/null +++ b/openpype/hosts/photoshop/api/lib.py @@ -0,0 +1,76 @@ +import os +import sys +import contextlib +import logging +import traceback + +from Qt import QtWidgets + +from openpype.tools.utils import host_tools + +from openpype.lib.remote_publish import headless_publish + +from .launch_logic import ProcessLauncher, stub + +log = logging.getLogger(__name__) +log.setLevel(logging.DEBUG) + + +def safe_excepthook(*args): + traceback.print_exception(*args) + + +def main(*subprocess_args): + from avalon import api, photoshop + + api.install(photoshop) + 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(lambda: 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( + lambda: 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 diff --git a/openpype/hosts/photoshop/api/panel.PNG b/openpype/hosts/photoshop/api/panel.PNG new file mode 100644 index 0000000000000000000000000000000000000000..be5db3b8df08aa426b92de61eacc9afd00e93d45 GIT binary patch literal 8756 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 literal 0 HcmV?d00001 diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py new file mode 100644 index 00000000000..ade144e6d43 --- /dev/null +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -0,0 +1,199 @@ +from .. import api, pipeline +from . import lib +from ..vendor import Qt + +import pyblish.api + + +def install(): + """Install Photoshop-specific functionality of avalon-core. + + This function is called automatically on calling `api.install(photoshop)`. + """ + print("Installing Avalon Photoshop...") + pyblish.api.register_host("photoshop") + + +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 from current workfile which + will be published. + + 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(): + if instance.get("schema") and \ + "container" in instance.get("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 + + +class Creator(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 = Qt.QtWidgets.QMessageBox() + msg.setIcon(Qt.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 + + +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 diff --git a/openpype/hosts/photoshop/api/workio.py b/openpype/hosts/photoshop/api/workio.py new file mode 100644 index 00000000000..ddcd351b381 --- /dev/null +++ b/openpype/hosts/photoshop/api/workio.py @@ -0,0 +1,50 @@ +"""Host API required Work Files tool""" +import os + +from . import lib +from avalon import api + + +def _active_document(): + document_name = lib.stub().get_active_document_name() + if not document_name: + return None + + return document_name + + +def file_extensions(): + return 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("\\", "/") diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py new file mode 100644 index 00000000000..f7bd03cdab1 --- /dev/null +++ b/openpype/hosts/photoshop/api/ws_stub.py @@ -0,0 +1,470 @@ +""" + Stub handling connection from server to client. + Used anywhere solution is calling client methods. +""" +import json +import sys +from wsrpc_aiohttp import WebSocketAsync +import attr + +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: 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 id in layers_meta: + if int(id) in layer_ids: + cleaned_data.append(layers_meta[id]) + + payload = json.dumps(cleaned_data, indent=4) + + self.websocketserver.call(self.client.call + ('Photoshop.imprint', payload=payload) + ) + + def get_layers(self): + """ + Returns JSON document with all(?) layers in active document. + + Returns: + 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 : + Returns: + """ + 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: + """ + 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: + """ + 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: + Returns: None + """ + 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 + """ + res = self.websocketserver.call(self.client.call + ('Photoshop.get_active_document_name')) + + return res + + def is_saved(self): + """ + Returns true if no changes in active document + Returns: + """ + return self.websocketserver.call(self.client.call + ('Photoshop.is_saved')) + + def save(self): + """ + Saves active document + Returns: None + """ + self.websocketserver.call(self.client.call + ('Photoshop.save')) + + def saveAs(self, image_path, ext, as_copy): + """ + Saves active document to psd (copy) or png or jpg + Args: + image_path(string): full local path + ext: + as_copy: + 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: + visibility: + 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") or \ + (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` + layer_name (str): Unique layer name to differentiate how many times + same smart object was loaded + + Args: + layer (PSItem): + path (str): File to import. + """ + 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: + + """ + 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 + item = 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")) + + ret.append(item) + return ret From 3ca400949134fc604744086ae74c8b16bd0d8757 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:00:53 +0100 Subject: [PATCH 02/13] removed empty hooks --- openpype/hosts/photoshop/hooks/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openpype/hosts/photoshop/hooks/__init__.py diff --git a/openpype/hosts/photoshop/hooks/__init__.py b/openpype/hosts/photoshop/hooks/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From c09d832c73db12455ec56d1429510f5c43772d56 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:04:35 +0100 Subject: [PATCH 03/13] use openpype logger --- openpype/hosts/photoshop/api/launch_logic.py | 13 +++++-------- openpype/hosts/photoshop/api/lib.py | 5 ++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py index 36347b8ce0e..8e0d00636ab 100644 --- a/openpype/hosts/photoshop/api/launch_logic.py +++ b/openpype/hosts/photoshop/api/launch_logic.py @@ -1,7 +1,6 @@ import os import subprocess import collections -import logging import asyncio import functools @@ -12,6 +11,7 @@ from Qt import QtCore +from openpype.api import Logger from openpype.tools.utils import host_tools from avalon import api @@ -19,8 +19,7 @@ from .ws_stub import PhotoshopServerStub -log = logging.getLogger(__name__) -log.setLevel(logging.DEBUG) +log = Logger.get_logger(__name__) class ConnectionNotEstablishedYet(Exception): @@ -81,10 +80,9 @@ def __init__(self, subprocess_args): @property def log(self): if self._log is None: - from openpype.api import Logger - - self._log = Logger.get_logger("{}-launcher".format( - self.route_name)) + self._log = Logger.get_logger( + "{}-launcher".format(self.route_name) + ) return self._log @property @@ -106,7 +104,6 @@ def is_host_connected(self): return False try: - _stub = stub() if _stub: return True diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index bc1fb36cf38..0938febf435 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -1,19 +1,18 @@ import os import sys import contextlib -import logging import traceback from Qt import QtWidgets from openpype.tools.utils import host_tools +from openpype.api import Logger from openpype.lib.remote_publish import headless_publish from .launch_logic import ProcessLauncher, stub -log = logging.getLogger(__name__) -log.setLevel(logging.DEBUG) +log = Logger.get_logger(__name__) def safe_excepthook(*args): From 1a623d6ee2a17246a881a2d214e9191e7a54aeab Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:05:05 +0100 Subject: [PATCH 04/13] formatting changes --- openpype/hosts/photoshop/api/workio.py | 5 +- openpype/hosts/photoshop/api/ws_stub.py | 351 +++++++++++++----------- 2 files changed, 191 insertions(+), 165 deletions(-) diff --git a/openpype/hosts/photoshop/api/workio.py b/openpype/hosts/photoshop/api/workio.py index ddcd351b381..0bf3ed2bd94 100644 --- a/openpype/hosts/photoshop/api/workio.py +++ b/openpype/hosts/photoshop/api/workio.py @@ -1,8 +1,9 @@ """Host API required Work Files tool""" import os +import avalon.api + from . import lib -from avalon import api def _active_document(): @@ -14,7 +15,7 @@ def _active_document(): def file_extensions(): - return api.HOST_WORKFILE_EXTENSIONS["photoshop"] + return avalon.api.HOST_WORKFILE_EXTENSIONS["photoshop"] def has_unsaved_changes(): diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py index f7bd03cdab1..b8f66332c64 100644 --- a/openpype/hosts/photoshop/api/ws_stub.py +++ b/openpype/hosts/photoshop/api/ws_stub.py @@ -2,10 +2,10 @@ Stub handling connection from server to client. Used anywhere solution is calling client methods. """ -import json import sys -from wsrpc_aiohttp import WebSocketAsync +import json import attr +from wsrpc_aiohttp import WebSocketAsync from avalon.tools.webserver.app import WebServerTool @@ -60,19 +60,19 @@ def get_client(): return client def open(self, path): - """ - Open file located at 'path' (local). + """Open file located at 'path' (local). + Args: path(string): file path locally Returns: None """ - self.websocketserver.call(self.client.call - ('Photoshop.open', path=path) - ) + 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 + """Parses layer metadata from Headline field of active document. + Args: layer: (PSItem) layers_meta: full list from Headline (for performance in loops) @@ -84,30 +84,29 @@ def read(self, layer, layers_meta=None): 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 + """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): @@ -139,19 +138,18 @@ def imprint(self, layer, data, all_layers=None, layers_meta=None): layer_ids = [layer.id for layer in all_layers] cleaned_data = [] - for id in layers_meta: - if int(id) in layer_ids: - cleaned_data.append(layers_meta[id]) + 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) - ) + self.websocketserver.call( + self.client.call('Photoshop.imprint', payload=payload) + ) def get_layers(self): - """ - Returns JSON document with all(?) layers in active document. + """Returns JSON document with all(?) layers in active document. Returns: Format of tuple: { 'id':'123', @@ -159,8 +157,9 @@ def get_layers(self): 'type': 'GUIDE'|'FG'|'BG'|'OBJ' 'visible': 'true'|'false' """ - res = self.websocketserver.call(self.client.call - ('Photoshop.get_layers')) + res = self.websocketserver.call( + self.client.call('Photoshop.get_layers') + ) return self._to_records(res) @@ -179,11 +178,13 @@ def get_layer(self, layer_id): return layer def get_layers_in_layers(self, layers): - """ - Return all layers that belong to layers (might be groups). + """Return all layers that belong to layers (might be groups). + Args: layers : - Returns: + + Returns: + """ all_layers = self.get_layers() ret = [] @@ -199,27 +200,30 @@ def get_layers_in_layers(self, layers): return ret def create_group(self, name): - """ - Create new group (eg. LayerSet) - Returns: + """Create new group (eg. LayerSet) + + Returns: + """ enhanced_name = self.PUBLISH_ICON + name - ret = self.websocketserver.call(self.client.call - ('Photoshop.create_group', - name=enhanced_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) + """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.websocketserver.call( + self.client.call( + 'Photoshop.group_selected_layers', name=enhanced_name + ) + ) res = self._to_records(res) if res: rec = res.pop() @@ -228,103 +232,112 @@ def group_selected_layers(self, name): raise ValueError("No group record returned") def get_selected_layers(self): - """ - Get a list of actually selected layers + """Get a list of actually selected layers. + Returns: """ - res = self.websocketserver.call(self.client.call - ('Photoshop.get_selected_layers')) + 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 + """Selects specified layers in Photoshop by its ids. + Args: layers: - Returns: None """ layers_id = [str(lay.id) for lay in layers] - self.websocketserver.call(self.client.call - ('Photoshop.select_layers', - layers=json.dumps(layers_id)) - ) + 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 + """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')) + 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 - """ - res = self.websocketserver.call(self.client.call - ('Photoshop.get_active_document_name')) + """Returns just a name of active document via ws call - return res + 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: + """ - Returns true if no changes in active document - Returns: - """ - return self.websocketserver.call(self.client.call - ('Photoshop.is_saved')) + return self.websocketserver.call( + self.client.call('Photoshop.is_saved') + ) def save(self): - """ - Saves active document - Returns: None - """ - self.websocketserver.call(self.client.call - ('Photoshop.save')) + """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 + """Saves active document to psd (copy) or png or jpg + Args: image_path(string): full local path ext: as_copy: Returns: None """ - self.websocketserver.call(self.client.call - ('Photoshop.saveAs', - image_path=image_path, - ext=ext, - as_copy=as_copy)) + 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' + """Set layer with 'layer_id' to 'visibility' + Args: layer_id: visibility: Returns: None """ - self.websocketserver.call(self.client.call - ('Photoshop.set_visible', - layer_id=layer_id, - visibility=visibility)) + 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. + """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')) @@ -337,8 +350,10 @@ def get_layers_metadata(self): if not isinstance(layers_data, dict): temp_layers_meta = {} for layer_meta in layers_data: - layer_id = layer_meta.get("uuid") or \ - (layer_meta.get("members")[0]) + 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: @@ -352,8 +367,7 @@ def get_layers_metadata(self): 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. + """Import the file at `path` as a smart object to active document. Args: path (str): File path to import. @@ -362,53 +376,62 @@ def import_smart_object(self, path, layer_name, as_reference=False): 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 - )) + 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` - layer_name (str): Unique layer name to differentiate how many times - same smart object was loaded + """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)) + 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. + """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)) + 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. + """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)) + self.websocketserver.call( + self.client.call( + 'Photoshop.rename_layer', + layer_id=layer_id, + name=name + ) + ) def remove_instance(self, instance_id): cleaned_data = {} @@ -419,14 +442,15 @@ def remove_instance(self, instance_id): payload = json.dumps(cleaned_data, indent=4) - self.websocketserver.call(self.client.call - ('Photoshop.imprint', payload=payload) - ) + 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')) + return self.websocketserver.call( + self.client.call('Photoshop.get_extension_version') + ) def close(self): """Shutting down PS and process too. @@ -437,11 +461,12 @@ def close(self): 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. + """Converts string json representation into list of PSItem for + dot notation access to work. + Args: res (string): valid json + Returns: """ @@ -456,15 +481,15 @@ def _to_records(self, res): layers_data = [layers_data] for d in layers_data: # currently implemented and expected fields - item = 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")) - - ret.append(item) + 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 From faf7e7bfebb0412ca360ab22373e75b53db612f9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:06:41 +0100 Subject: [PATCH 05/13] extended main thread exection --- openpype/hosts/photoshop/api/launch_logic.py | 71 +++++++++++++++++--- openpype/hosts/photoshop/api/lib.py | 8 ++- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py index 8e0d00636ab..16a1d232440 100644 --- a/openpype/hosts/photoshop/api/launch_logic.py +++ b/openpype/hosts/photoshop/api/launch_logic.py @@ -2,7 +2,6 @@ import subprocess import collections import asyncio -import functools from wsrpc_aiohttp import ( WebSocketRoute, @@ -26,6 +25,61 @@ 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 @@ -113,8 +167,10 @@ def is_host_connected(self): return None @classmethod - def execute_in_main_thread(cls, callback): - cls._main_thread_callbacks.append(callback) + 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: @@ -145,8 +201,8 @@ def _on_loop_timer(self): cls = self.__class__ for _ in range(len(cls._main_thread_callbacks)): if cls._main_thread_callbacks: - callback = cls._main_thread_callbacks.popleft() - callback() + item = cls._main_thread_callbacks.popleft() + item.execute() if not self.is_process_running: self.log.info("Host process is not running. Closing") @@ -303,10 +359,7 @@ async def experimental_tools_route(self): def _tool_route(self, _tool_name): """The address accessed when clicking on the buttons.""" - partial_method = functools.partial(show_tool_by_name, - _tool_name) - - ProcessLauncher.execute_in_main_thread(partial_method) + ProcessLauncher.execute_in_main_thread(show_tool_by_name, _tool_name) # Required return statement. return "nothing" diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index 0938febf435..4c80c04caea 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -34,17 +34,19 @@ def main(*subprocess_args): launcher.start() if os.environ.get("HEADLESS_PUBLISH"): - launcher.execute_in_main_thread(lambda: headless_publish( + launcher.execute_in_main_thread( + headless_publish, log, "ClosePS", - os.environ.get("IS_TEST"))) + 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( - lambda: host_tools.show_workfiles(save=save) + host_tools.show_workfiles, save=save ) sys.exit(app.exec_()) From 1b36e7e73039169164b98cf1408134f43b634541 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:07:29 +0100 Subject: [PATCH 06/13] added code from openpype __init__.py to pipeline.py --- openpype/hosts/photoshop/api/pipeline.py | 126 +++++++++++++++++------ 1 file changed, 95 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index ade144e6d43..ed7b94e249a 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -1,8 +1,61 @@ -from .. import api, pipeline -from . import lib -from ..vendor import Qt +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(): @@ -10,9 +63,26 @@ def install(): This function is called automatically on calling `api.install(photoshop)`. """ - print("Installing Avalon 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 @@ -54,16 +124,14 @@ def ls(): def list_instances(): - """ - List all created instances from current workfile which - will be published. + """List all created instances to publish from current workfile. - Pulls from File > File Info + Pulls from File > File Info - For SubsetManager + For SubsetManager - Returns: - (list) of dictionaries matching instances format + Returns: + (list) of dictionaries matching instances format """ stub = _get_stub() @@ -74,8 +142,8 @@ def list_instances(): layers_meta = stub.get_layers_metadata() if layers_meta: for key, instance in layers_meta.items(): - if instance.get("schema") and \ - "container" in instance.get("schema"): + schema = instance.get("schema") + if schema and "container" in schema: continue instance['uuid'] = key @@ -85,16 +153,15 @@ def list_instances(): def remove_instance(instance): - """ - Remove instance from current workfile metadata. + """Remove instance from current workfile metadata. - Updates metadata of current file in File > File Info and removes - icon highlight on group layer. + Updates metadata of current file in File > File Info and removes + icon highlight on group layer. - For SubsetManager + For SubsetManager - Args: - instance (dict): instance representation from subsetmanager model + Args: + instance (dict): instance representation from subsetmanager model """ stub = _get_stub() @@ -109,8 +176,8 @@ def remove_instance(instance): def _get_stub(): - """ - Handle pulling stub from PS to run operations on host + """Handle pulling stub from PS to run operations on host + Returns: (PhotoshopServerStub) or None """ @@ -126,7 +193,7 @@ def _get_stub(): return stub -class Creator(api.Creator): +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 @@ -140,8 +207,8 @@ def process(self): stub = lib.stub() # only after Photoshop is up for layer in stub.get_layers(): if self.name.lower() == layer.Name.lower(): - msg = Qt.QtWidgets.QMessageBox() - msg.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = QtWidgets.QMessageBox() + msg.setIcon(QtWidgets.QMessageBox.Warning) msg.setText(msg) msg.exec_() return False @@ -160,12 +227,9 @@ def process(self): return group -def containerise(name, - namespace, - layer, - context, - loader=None, - suffix="_CON"): +def containerise( + name, namespace, layer, context, loader=None, suffix="_CON" +): """Imprint layer with metadata Containerisation enables a tracking of version, author and origin From 56446e0c4c4dc51ebbb600fb4656d7a1a851f0cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:07:45 +0100 Subject: [PATCH 07/13] changed registered host --- openpype/hosts/photoshop/api/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index 4c80c04caea..509c5d5c482 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -1,13 +1,15 @@ import os import sys +import re import contextlib import traceback from Qt import QtWidgets -from openpype.tools.utils import host_tools +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 @@ -20,9 +22,9 @@ def safe_excepthook(*args): def main(*subprocess_args): - from avalon import api, photoshop + from openpype.hosts.photoshop import api - api.install(photoshop) + avalon.api.install(api) sys.excepthook = safe_excepthook # coloring in ConsoleTrayApp From c1d6eaa5f948f823eea373b723a405176c27278b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:08:51 +0100 Subject: [PATCH 08/13] changed import of photoshop in plugins --- openpype/hosts/photoshop/plugins/create/create_image.py | 2 +- openpype/hosts/photoshop/plugins/load/load_image.py | 3 ++- .../hosts/photoshop/plugins/load/load_image_from_sequence.py | 2 +- openpype/hosts/photoshop/plugins/load/load_reference.py | 3 ++- openpype/hosts/photoshop/plugins/publish/closePS.py | 2 +- .../hosts/photoshop/plugins/publish/collect_current_file.py | 2 +- .../photoshop/plugins/publish/collect_extension_version.py | 2 +- .../hosts/photoshop/plugins/publish/collect_instances.py | 2 +- .../photoshop/plugins/publish/collect_remote_instances.py | 5 +++-- openpype/hosts/photoshop/plugins/publish/collect_workfile.py | 2 +- openpype/hosts/photoshop/plugins/publish/extract_image.py | 2 +- openpype/hosts/photoshop/plugins/publish/extract_review.py | 2 +- .../hosts/photoshop/plugins/publish/extract_save_scene.py | 2 +- .../hosts/photoshop/plugins/publish/increment_workfile.py | 2 +- .../photoshop/plugins/publish/validate_instance_asset.py | 2 +- openpype/hosts/photoshop/plugins/publish/validate_naming.py | 2 +- 16 files changed, 20 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 657d41aa93f..cf41bb40202 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -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): diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index 981a1ed2049..3756eba54e5 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -1,6 +1,7 @@ import re -from avalon import api, photoshop +from avalon import api +from openpype.hosts.photoshop import api as photoshop from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 8704627b123..158bdc29409 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -1,12 +1,12 @@ 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 +from openpype.hosts.photoshop import api as photoshop stub = photoshop.stub() diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py index 0cb4e4a69f3..844bb2463a4 100644 --- a/openpype/hosts/photoshop/plugins/load/load_reference.py +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -1,7 +1,8 @@ import re -from avalon import api, photoshop +from avalon import api +from openpype.hosts.photoshop import api as photoshop from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name stub = photoshop.stub() diff --git a/openpype/hosts/photoshop/plugins/publish/closePS.py b/openpype/hosts/photoshop/plugins/publish/closePS.py index 2f0eab0ee5b..b4ded96001c 100644 --- a/openpype/hosts/photoshop/plugins/publish/closePS.py +++ b/openpype/hosts/photoshop/plugins/publish/closePS.py @@ -4,7 +4,7 @@ import pyblish.api -from avalon import photoshop +from openpype.hosts.photoshop import api as photoshop class ClosePS(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py index 4d4829555ec..5daf47c6ac3 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py @@ -2,7 +2,7 @@ import pyblish.api -from avalon import photoshop +from openpype.hosts.photoshop import api as photoshop class CollectCurrentFile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py index f07ff0b0ff7..64c99b4fc1e 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py @@ -2,7 +2,7 @@ import re import pyblish.api -from avalon import photoshop +from openpype.hosts.photoshop import api as photoshop class CollectExtensionVersion(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index 5390df768b3..f67cc0cbac8 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -1,6 +1,6 @@ import pyblish.api -from avalon import photoshop +from openpype.hosts.photoshop import api as photoshop class CollectInstances(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py index c76e15484ea..e264d04d9fa 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_remote_instances.py @@ -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): diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 88817c39690..db1ede14d5c 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -1,5 +1,5 @@ -import pyblish.api import os +import pyblish.api class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py index ae9892e2903..2ba81e0baca 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_image.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py @@ -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): diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py index 8c4d05b282a..1ad442279ab 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_review.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py @@ -2,7 +2,7 @@ import openpype.api import openpype.lib -from avalon import photoshop +from openpype.hosts.photoshop import api as photoshop class ExtractReview(openpype.api.Extractor): diff --git a/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py b/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py index 0180640c908..03086f389f8 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_save_scene.py @@ -1,5 +1,5 @@ import openpype.api -from avalon import photoshop +from openpype.hosts.photoshop import api as photoshop class ExtractSaveScene(openpype.api.Extractor): diff --git a/openpype/hosts/photoshop/plugins/publish/increment_workfile.py b/openpype/hosts/photoshop/plugins/publish/increment_workfile.py index 709fb988fca..92132c393bb 100644 --- a/openpype/hosts/photoshop/plugins/publish/increment_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/increment_workfile.py @@ -3,7 +3,7 @@ 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): diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py index 4dc1972074c..ebe9cc21eae 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -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): diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index 1635096f4b2..b40e44d016a 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -2,7 +2,7 @@ import pyblish.api import openpype.api -from avalon import photoshop +from openpype.hosts.photoshop import api as photoshop class ValidateNamingRepair(pyblish.api.Action): From 551d40b62487618f2c821c40b034c93c92ae2bce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:09:08 +0100 Subject: [PATCH 09/13] changed from where is 'main' imported --- openpype/scripts/non_python_host_launch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/scripts/non_python_host_launch.py b/openpype/scripts/non_python_host_launch.py index 32c4b23f4f3..6b17e6a037d 100644 --- a/openpype/scripts/non_python_host_launch.py +++ b/openpype/scripts/non_python_host_launch.py @@ -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": From d7bc9c4124c38b4f5ae804508939afaf48f572d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:17:50 +0100 Subject: [PATCH 10/13] moved lib functions outside of plugins dir --- openpype/hosts/photoshop/api/__init__.py | 7 ++++++- openpype/hosts/photoshop/{plugins/lib.py => api/plugin.py} | 0 openpype/hosts/photoshop/plugins/__init__.py | 0 openpype/hosts/photoshop/plugins/load/load_image.py | 2 +- .../photoshop/plugins/load/load_image_from_sequence.py | 2 +- openpype/hosts/photoshop/plugins/load/load_reference.py | 2 +- 6 files changed, 9 insertions(+), 4 deletions(-) rename openpype/hosts/photoshop/{plugins/lib.py => api/plugin.py} (100%) delete mode 100644 openpype/hosts/photoshop/plugins/__init__.py diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index 43756b9ee4f..a25dfe70443 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -12,7 +12,9 @@ install, containerise ) - +from .plugin import ( + get_unique_layer_name +) from .workio import ( file_extensions, has_unsaved_changes, @@ -38,6 +40,9 @@ "install", "containerise", + # Plugin + "get_unique_layer_name", + # workfiles "file_extensions", "has_unsaved_changes", diff --git a/openpype/hosts/photoshop/plugins/lib.py b/openpype/hosts/photoshop/api/plugin.py similarity index 100% rename from openpype/hosts/photoshop/plugins/lib.py rename to openpype/hosts/photoshop/api/plugin.py diff --git a/openpype/hosts/photoshop/plugins/__init__.py b/openpype/hosts/photoshop/plugins/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index 3756eba54e5..25f47b02570 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -2,8 +2,8 @@ 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() diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 158bdc29409..bbf4c602425 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -5,8 +5,8 @@ from avalon.vendor import qargparse from openpype.lib import Anatomy -from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name from openpype.hosts.photoshop import api as photoshop +from openpype.hosts.photoshop.api import get_unique_layer_name stub = photoshop.stub() diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py index 844bb2463a4..0f3c1481551 100644 --- a/openpype/hosts/photoshop/plugins/load/load_reference.py +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -3,7 +3,7 @@ from avalon import api from openpype.hosts.photoshop import api as photoshop -from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name +from openpype.hosts.photoshop.api import get_unique_layer_name stub = photoshop.stub() From ab1b2bdd7d1f82360a2e066fc704581af6b3fd66 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:19:53 +0100 Subject: [PATCH 11/13] moved getting of stub to default photoshop loader class instead of loading it in global scope --- openpype/hosts/photoshop/api/__init__.py | 12 +++--- openpype/hosts/photoshop/api/lib.py | 1 - openpype/hosts/photoshop/api/plugin.py | 9 +++++ .../photoshop/plugins/load/load_image.py | 27 +++++++------ .../plugins/load/load_image_from_sequence.py | 20 ++++------ .../photoshop/plugins/load/load_reference.py | 38 ++++++++++--------- 6 files changed, 60 insertions(+), 47 deletions(-) diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index a25dfe70443..17a371f002e 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -4,6 +4,8 @@ """ +from .launch_logic import stub + from .pipeline import ( ls, list_instances, @@ -13,6 +15,7 @@ containerise ) from .plugin import ( + PhotoshopLoader, get_unique_layer_name ) from .workio import ( @@ -29,9 +32,10 @@ maintained_visibility ) -from .launch_logic import stub - __all__ = [ + # launch_logic + "stub" + # pipeline "ls", "list_instances", @@ -41,6 +45,7 @@ "containerise", # Plugin + "PhotoshopLoader", "get_unique_layer_name", # workfiles @@ -54,7 +59,4 @@ # lib "maintained_selection", "maintained_visibility", - - # launch_logic - "stub" ] diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index 509c5d5c482..707cd476c51 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -1,6 +1,5 @@ import os import sys -import re import contextlib import traceback diff --git a/openpype/hosts/photoshop/api/plugin.py b/openpype/hosts/photoshop/api/plugin.py index 74aff061147..c577c67d82a 100644 --- a/openpype/hosts/photoshop/api/plugin.py +++ b/openpype/hosts/photoshop/api/plugin.py @@ -1,5 +1,8 @@ import re +import avalon.api +from .launch_logic import stub + def get_unique_layer_name(layers, asset_name, subset_name): """ @@ -24,3 +27,9 @@ def get_unique_layer_name(layers, asset_name, subset_name): occurrences = names.get(name, 0) return "{}_{:0>3d}".format(name, occurrences + 1) + + +class PhotoshopLoader(avalon.api.Loader): + @staticmethod + def get_stub(): + return stub() diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index 25f47b02570..3b1cfe96360 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -5,9 +5,7 @@ from openpype.hosts.photoshop.api 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. @@ -17,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 @@ -36,6 +37,8 @@ def load(self, context, name=None, namespace=None, data=None): def update(self, container, representation): """ Switch asset or change version """ + stub = self.get_stub() + layer = container.pop("layer") context = representation.get("context", {}) @@ -45,9 +48,9 @@ def update(self, container, representation): 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"] @@ -67,6 +70,8 @@ def remove(self, container): 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) @@ -74,5 +79,5 @@ def remove(self, container): 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) diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index bbf4c602425..ab4682e63e4 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -1,17 +1,13 @@ import os -from avalon import api from avalon.pipeline import get_representation_path_from_context from avalon.vendor import qargparse -from openpype.lib import Anatomy from openpype.hosts.photoshop import api as photoshop from openpype.hosts.photoshop.api import get_unique_layer_name -stub = photoshop.stub() - -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 @@ def update(self, container, representation): def remove(self, container): """No update possible, not containerized.""" pass - diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py index 0f3c1481551..60142d4a1ff 100644 --- a/openpype/hosts/photoshop/plugins/load/load_reference.py +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -5,27 +5,26 @@ from openpype.hosts.photoshop import api as photoshop from openpype.hosts.photoshop.api import get_unique_layer_name -stub = photoshop.stub() - -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 @@ -40,6 +39,7 @@ def load(self, context, name=None, namespace=None, data=None): def update(self, container, representation): """ Switch asset or change version """ + stub = self.get_stub() layer = container.pop("layer") context = representation.get("context", {}) @@ -49,9 +49,9 @@ def update(self, container, representation): 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,11 +66,12 @@ def update(self, container, representation): ) 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) @@ -78,6 +79,7 @@ def remove(self, container): 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 + ) From c2b6cf8714a8caf5d6e49efbb44b386fdcdf713a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:38:32 +0100 Subject: [PATCH 12/13] fixed init file --- openpype/hosts/photoshop/api/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index 17a371f002e..4cc2aa2c781 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -10,12 +10,13 @@ ls, list_instances, remove_instance, - Creator, install, + uninstall, containerise ) from .plugin import ( PhotoshopLoader, + Creator, get_unique_layer_name ) from .workio import ( @@ -34,18 +35,18 @@ __all__ = [ # launch_logic - "stub" + "stub", # pipeline "ls", "list_instances", "remove_instance", - "Creator", "install", "containerise", # Plugin "PhotoshopLoader", + "Creator", "get_unique_layer_name", # workfiles From b6a5123210d8f278cbb320f7378bce877c798949 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 10 Jan 2022 23:38:43 +0100 Subject: [PATCH 13/13] moved Creator to plugin.py --- openpype/hosts/photoshop/api/pipeline.py | 34 ------------------------ openpype/hosts/photoshop/api/plugin.py | 34 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index ed7b94e249a..25983f24713 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -193,40 +193,6 @@ 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 - - def containerise( name, namespace, layer, context, loader=None, suffix="_CON" ): diff --git a/openpype/hosts/photoshop/api/plugin.py b/openpype/hosts/photoshop/api/plugin.py index c577c67d82a..e0db67de2c3 100644 --- a/openpype/hosts/photoshop/api/plugin.py +++ b/openpype/hosts/photoshop/api/plugin.py @@ -33,3 +33,37 @@ 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