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

Photoshop #142

Merged
merged 61 commits into from
Jun 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
1ed084a
Working prototype
tokejepsen Jan 24, 2020
faa6f1e
Convert Flask to vendorized Bottle server.
tokejepsen Jan 24, 2020
8bf549c
Export start_server function
tokejepsen Jan 27, 2020
eeb0bc8
Fix workfile saving.
tokejepsen Jan 27, 2020
4df1b7e
Revert using separate process for launching tools.
tokejepsen Jan 27, 2020
ba0463d
Using DOM for Photoshop communication
tokejepsen Jan 29, 2020
89091f2
Condensed Photoshop server to one file.
tokejepsen Jan 29, 2020
ade16ee
Implemented Photoshop required methods for creation
tokejepsen Jan 30, 2020
fcd88d0
Prevent duplicate instances from being created.
tokejepsen Jan 31, 2020
7a7ec80
Remove redundant "warning" function.
tokejepsen Jan 31, 2020
74d5140
Make the integration more thread safe.
tokejepsen Jan 31, 2020
531a5aa
Fix Photoshop workio open.
tokejepsen Feb 3, 2020
ffa8a86
Introduce com objects for Photoshop constants.
tokejepsen Feb 3, 2020
caad4cd
Change lib functions
tokejepsen Feb 3, 2020
7794267
Maintain selection during creation.
tokejepsen Feb 3, 2020
0375143
Add maintained_visibility
tokejepsen Feb 3, 2020
75c72e2
Code cosmetics
tokejepsen Feb 3, 2020
8cebe13
Expose lib functions
tokejepsen Feb 3, 2020
914582b
Redundant code
tokejepsen Feb 3, 2020
8a7c4c9
Expose functions
tokejepsen Feb 4, 2020
d1a22ba
Adjust containerise and ls.
tokejepsen Feb 4, 2020
c750ce4
Fix updating imprinted data.
tokejepsen Feb 4, 2020
aa9663a
Rename "import_as_smart_object" to "import_smart_object"
tokejepsen Feb 4, 2020
819866b
Add "replace_smart_object"
tokejepsen Feb 4, 2020
477b54d
Expose functions
tokejepsen Feb 4, 2020
9fedf6c
Move server to tools for reusability.
tokejepsen Feb 5, 2020
ac5dcf4
Replace get_layer_type with com_objects.
tokejepsen Feb 5, 2020
22f2eaa
Improve documentation of functions.
tokejepsen Feb 5, 2020
e73a1d5
Add signed extension package.
tokejepsen Feb 5, 2020
2da8e68
Photoshop integration documentation.
tokejepsen Feb 5, 2020
55f90cf
Fix documentation image links.
tokejepsen Feb 5, 2020
8bf33bd
Silence hound.
tokejepsen Feb 5, 2020
80b2b76
Unpack Layers generators with list comprehensions.
tokejepsen Feb 7, 2020
2ae008d
Fix grammar.
tokejepsen Feb 7, 2020
c0a91d2
Convert Layers with list() rather than list comprension.
tokejepsen Feb 7, 2020
0b48cae
Store app as variable when used multiple times within function.
tokejepsen Feb 7, 2020
dab5e00
Convert partial variable to function for clarity.
tokejepsen Feb 7, 2020
1405671
Wrap Dispatch
tokejepsen Feb 10, 2020
6565618
Clean up data on imprint.
tokejepsen Feb 10, 2020
0ee4e02
Wrap and rename com_objects
tokejepsen Feb 10, 2020
f58268c
Remove doc example.
tokejepsen Feb 10, 2020
2f44a0d
Ignore Photoshop on nose tests.
tokejepsen Feb 10, 2020
018d683
Ignore Photoshop from Maya tests.
tokejepsen Feb 10, 2020
8af7fbf
Revert "Remove doc example."
tokejepsen Feb 10, 2020
e3a65b8
Expose Dispatch.
tokejepsen Feb 11, 2020
d746e3e
Improve doc string.
tokejepsen Feb 11, 2020
aaebb03
Merge branch 'master' into photoshop
tokejepsen Feb 12, 2020
4274059
Remove config argument from Photoshop install
tokejepsen Feb 12, 2020
21bc3f6
Launch Photoshop and server together.
tokejepsen Feb 14, 2020
baa376d
Silence hound.
tokejepsen Feb 14, 2020
98503a6
Always exec QApplication.
tokejepsen Feb 17, 2020
4d14d42
Merge branch 'master' into photoshop
tokejepsen Feb 17, 2020
b64d259
Fix for context and workfiles merge.
tokejepsen Feb 17, 2020
7054dea
Update README.md
tokejepsen Mar 25, 2020
10203e9
Merge branch 'develop' into feature/photoshop
tokejepsen Jun 9, 2020
cf38dd2
Expose API functions
tokejepsen Jun 10, 2020
9a87249
Return group on group_selected_layers
tokejepsen Jun 10, 2020
012a090
Fix "Use Selection" on LayerSets
tokejepsen Jun 10, 2020
30595dd
Better failsafe for workio/current_file
tokejepsen Jun 10, 2020
a4e5eeb
Remove "Context" button.
tokejepsen Jun 10, 2020
28c5987
Show Workfiles on Photoshop launch.
tokejepsen Jun 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 256 additions & 0 deletions avalon/photoshop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# Photoshop Integration

`NOTE: This integration is only tested on Windows.`

This integration requires the third part library `pywin32` which can be installed with:

```
pip install pywin32
```

## 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.

You can also run the server separately with:

```
python -c "from avalon.tools import html_server;html_server.app.start_server(5000)"
```

## 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")

If the server is not running you will get a page failure:

![Avalon Panel Failure](panel_failure.PNG "Avalon Panel Failure")

Start the server and hit `Refresh`.

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

from avalon import photoshop

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

def process(self, context):
# Necessary call when running in a different thread which pyblish-qml
# can be.
pythoncom.CoInitialize()

for layer in photoshop.get_layers_in_document():
layer_data = photoshop.read(layer)

# Skip layers without metadata.
if layer_data is None:
continue

# Skip containers.
if "container" in layer_data["id"]:
continue

child_layers = [*layer.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)

# 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 pyblish.api
from avalon import photoshop, Session


class ExtractImage(pyblish.api.InstancePlugin):
"""Produce a flattened image file from instance

This plug-in takes into account only the layers in the group.
"""

label = "Extract Image"
order = pyblish.api.ExtractorOrder
hosts = ["photoshop"]
families = ["image"]

def process(self, instance):

dirname = os.path.join(
os.path.normpath(
Session["AVALON_WORKDIR"]
).replace("\\", "/"),
instance.data["name"]
)

try:
os.makedirs(dirname)
except OSError:
pass

# Store reference for integration
if "files" not in instance.data:
instance.data["files"] = list()

path = os.path.join(dirname, instance.data["name"])

# Perform extraction
with photoshop.maintained_selection():
self.log.info("Extracting %s" % str(list(instance)))
with photoshop.maintained_visibility():
# Hide all other layers.
extract_ids = [
x.id for x in photoshop.get_layers_in_layers([instance[0]])
]
for layer in photoshop.get_layers_in_document():
if layer.id not in extract_ids:
layer.Visible = False

save_options = {
"png": photoshop.com_objects.PNGSaveOptions(),
"jpg": photoshop.com_objects.JPEGSaveOptions()
}

for extension, save_option in save_options.items():
photoshop.app().ActiveDocument.SaveAs(
path, save_option, True
)
instance.data["files"].append(
"{}.{}".format(path, extension)
)

instance.data["stagingDir"] = dirname

self.log.info("Extracted {instance} to {path}".format(**locals()))
```

#### Loader Plugin
```python
from avalon import api, photoshop


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 = photoshop.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():
photoshop.replace_smart_object(
layer, api.get_representation_path(representation)
)

photoshop.imprint(
layer, {"representation": str(representation["_id"])}
)

def remove(self, container):
container["layer"].Delete()

def switch(self, container, representation):
self.update(container, representation)
```

## 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
70 changes: 70 additions & 0 deletions avalon/photoshop/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Public API

Anything that isn't defined here is INTERNAL and unreliable for external use.

"""

from .pipeline import (
ls,
Creator,
install,
containerise
)

from .workio import (
file_extensions,
has_unsaved_changes,
save_file,
open_file,
current_file,
work_root,
)

from .lib import (
launch,
app,
Dispatch,
maintained_selection,
maintained_visibility,
get_layers_in_document,
get_layers_in_layers,
get_selected_layers,
group_selected_layers,
imprint,
read,
get_com_objects,
import_smart_object,
replace_smart_object
)

__all__ = [
# pipeline
"ls",
"Creator",
"install",
"containerise",

# workfiles
"file_extensions",
"has_unsaved_changes",
"save_file",
"open_file",
"current_file",
"work_root",

# lib
"launch",
"app",
"Dispatch",
"maintained_selection",
"maintained_visibility",
"get_layers_in_document",
"get_layers_in_layers",
"get_selected_layers",
"group_selected_layers",
"imprint",
"read",
"get_com_objects",
"import_smart_object",
"replace_smart_object"
]
Loading