This repository has been archived by the owner on Sep 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 129
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2510 from pypeclub/feature/move_photoshop_to_open…
…pype Photoshop: Move implementation to OpenPype
- Loading branch information
Showing
43 changed files
with
5,531 additions
and
167 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,79 +1,63 @@ | ||
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 .launch_logic import stub | ||
|
||
from .pipeline import ( | ||
ls, | ||
list_instances, | ||
remove_instance, | ||
install, | ||
uninstall, | ||
containerise | ||
) | ||
from .plugin import ( | ||
PhotoshopLoader, | ||
Creator, | ||
get_unique_layer_name | ||
) | ||
from .workio import ( | ||
file_extensions, | ||
has_unsaved_changes, | ||
save_file, | ||
open_file, | ||
current_file, | ||
work_root, | ||
) | ||
|
||
from .lib import ( | ||
maintained_selection, | ||
maintained_visibility | ||
) | ||
|
||
__all__ = [ | ||
# launch_logic | ||
"stub", | ||
|
||
# pipeline | ||
"ls", | ||
"list_instances", | ||
"remove_instance", | ||
"install", | ||
"containerise", | ||
|
||
# Plugin | ||
"PhotoshopLoader", | ||
"Creator", | ||
"get_unique_layer_name", | ||
|
||
# workfiles | ||
"file_extensions", | ||
"has_unsaved_changes", | ||
"save_file", | ||
"open_file", | ||
"current_file", | ||
"work_root", | ||
|
||
# lib | ||
"maintained_selection", | ||
"maintained_visibility", | ||
] |
Binary file not shown.
Oops, something went wrong.