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
Photoshop: Move implementation to OpenPype #2510
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
e94aa53
moved photoshop from avalon to openpype
iLLiCiTiT 3ca4009
removed empty hooks
iLLiCiTiT c09d832
use openpype logger
iLLiCiTiT 1a623d6
formatting changes
iLLiCiTiT faf7e7b
extended main thread exection
iLLiCiTiT 1b36e7e
added code from openpype __init__.py to pipeline.py
iLLiCiTiT 56446e0
changed registered host
iLLiCiTiT c1d6eaa
changed import of photoshop in plugins
iLLiCiTiT 551d40b
changed from where is 'main' imported
iLLiCiTiT d7bc9c4
moved lib functions outside of plugins dir
iLLiCiTiT ab1b2bd
moved getting of stub to default photoshop loader class instead of lo…
iLLiCiTiT c2b6cf8
fixed init file
iLLiCiTiT b6a5123
moved Creator to plugin.py
iLLiCiTiT File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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