Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Photoshop: Move implementation to OpenPype #2510

Merged
merged 13 commits into from
Jan 11, 2022
Merged
255 changes: 255 additions & 0 deletions openpype/hosts/photoshop/api/README.md
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
142 changes: 63 additions & 79 deletions openpype/hosts/photoshop/api/__init__.py
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 (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'.pipeline.uninstall' imported but unused

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 added openpype/hosts/photoshop/api/extension.zxp
Binary file not shown.
Loading