merge tianlang0704/multiuser

This commit is contained in:
zombieyang
2024-10-03 11:05:09 +08:00
16 changed files with 2777 additions and 7 deletions
+127
View File
@@ -0,0 +1,127 @@
import time
class PhotoshopInstance:
SPECIAL_LAYER_NEW_LAYER = '### New Layer ###'
SPECIAL_LAYER_USE_CANVAS = '### Use Canvas ###'
SPECIAL_LAYER_USE_SELECTION = '### Use Selection ###'
SPECIAL_LAYER_SAME_AS_LAYER = '### Same as Layer ###'
SPECIAL_LAYER_NAME_TO_ID = {
SPECIAL_LAYER_USE_CANVAS: 0,
SPECIAL_LAYER_USE_SELECTION: -1,
SPECIAL_LAYER_NEW_LAYER: -2,
SPECIAL_LAYER_SAME_AS_LAYER: -3
}
def __init__(self, sdppp, sid, uid):
self.sdppp = sdppp
self.sid = sid
self.uid = uid
self.layers = []
self.on_destroy = None
self.reset_change_tracker()
def destroy(self):
if self.on_destroy:
self.on_destroy(self)
def get_layers(self):
return list(map(lambda layer: f"{layer['name']} (id:{layer['id']})", self.layers))
def get_base_layers(self):
raw_layer_strs = self.get_layers()
layer_strs = list(raw_layer_strs)
layer_strs.insert(0, self.SPECIAL_LAYER_USE_CANVAS)
return layer_strs
def get_bounds_layers(self):
bounds_strs = list(self.get_layers())
bounds_strs.insert(0, self.SPECIAL_LAYER_USE_CANVAS)
bounds_strs.insert(1, self.SPECIAL_LAYER_USE_SELECTION)
bounds_strs.insert(1, self.SPECIAL_LAYER_SAME_AS_LAYER)
return bounds_strs
def get_set_layers(self):
raw_layer_strs = self.get_layers()
set_layer_strs = list(raw_layer_strs)
set_layer_strs.insert(0, self.SPECIAL_LAYER_NEW_LAYER)
return set_layer_strs
def layer_name_to_id(self, layer_name, refrence_id=None):
id = 0
if layer_name == self.SPECIAL_LAYER_SAME_AS_LAYER:
id = refrence_id
elif self.SPECIAL_LAYER_NAME_TO_ID.get(layer_name, None) is not None:
id = self.SPECIAL_LAYER_NAME_TO_ID[layer_name]
else:
layer_name_and_id_split = layer_name.split('(id:')
id = int(layer_name_and_id_split.pop().strip()[:-1])
return id
async def get_image(self, layer_id, bounds_id=False):
result = await self.sdppp.sio.call('get_image', data={'layer_id': layer_id, 'use_layer_bounds': bounds_id}, to=self.sid)
if not result:
return None, None
history_state_id = await self._update_layer_bounds_history_state_id(layer_id, bounds_id)
await self._update_history_state_id_after_internal_change(history_state_id)
return result['upload_name'], result['layer_opacity']
async def send_images(self, image_ids, layer_id=""):
result = await self.sdppp.sio.call('send_images', data={'image_ids': image_ids, 'layer_id': layer_id}, to=self.sid)
await self._update_history_state_id_after_internal_change()
return result
async def get_active_history_state_id(self):
result = await self.sdppp.sio.call('get_active_history_state_id', data={}, to=self.sid)
if result is None:
return 0
id = result.get('history_state_id', None)
return id
# ===
def reset_change_tracker(self):
self.get_img_state_id = 0
self.change_tracker = {}
self.comfyui_last_value_tracker = {}
# for apis calls
async def is_ps_history_changed(self):
current_id = await self.get_active_history_state_id()
if self.get_img_state_id is None or current_id is None:
return False
return self.get_img_state_id != current_id and self.get_img_state_id < current_id
async def check_layer_bounds_combo_changed(self, layer, use_layer_bounds):
layer_bounds_combo = f"{layer}{use_layer_bounds}"
layer_bounds_history_state_id = self.change_tracker.get(layer_bounds_combo, None)
if layer_bounds_history_state_id is None:
return True
latest_state_id = await self.get_active_history_state_id()
if latest_state_id > layer_bounds_history_state_id:
return True, latest_state_id
#didn't change, use last value
comfyui_last_value = self.comfyui_last_value_tracker.get(layer_bounds_combo, None) or layer_bounds_history_state_id
return False, comfyui_last_value
# have to track this value because comfyui determines if the value is changed by comparing it with the last value.
# can't use the real history id because sending images changes the history id, and comfyui will think the value is changed when it's not
def update_comfyui_last_value(self, layer, use_layer_bounds, value):
layer_bounds_combo = f"{layer}{use_layer_bounds}"
self.comfyui_last_value_tracker[layer_bounds_combo] = value
return value
# need to update history id after internal operation otherwise it might cause infinite change loop
async def _update_history_state_id_after_internal_change(self, history_state_id=None):
if history_state_id is None: # need to get new id after operation, it causes history change
history_state_id = await self.get_active_history_state_id()
if self.get_img_state_id is not None: # udpate all the change trackers with the new history state id
self.change_tracker = {k: history_state_id for k, v in self.change_tracker.items() if v == self.get_img_state_id}
self.get_img_state_id = history_state_id # it gets into a loop if get img state is not updated
async def _update_layer_bounds_history_state_id(self, layer, use_layer_bounds):
layer_bounds_combo = f"{layer}{use_layer_bounds}"
latest_state_id = await self.get_active_history_state_id()
self.change_tracker[layer_bounds_combo] = latest_state_id
return latest_state_id
+121
View File
@@ -0,0 +1,121 @@
import socketio
import json
import asyncio
from socketio import exceptions
from .photoshop_manager import PhotoshopManager
class SDPPP:
def __init__(self, PromptServer):
self.photoshop_instances = dict()
self.comfyui_instances = dict()
self.sid_to_ip = dict()
self.sio = socketio.AsyncServer(async_mode='aiohttp', cors_allowed_origins="*")
self.sio.attach(PromptServer.instance.app, socketio_path='/sd-ppp/')
self.registerSocketListeners()
self.loop = PromptServer.instance.loop
def has_ps_instance(self):
return len(self.photoshop_instances) > 0
def get_ps_instance(self, sid = None):
if len(self.photoshop_instances) == 0:
return None
if sid is None:
sid = list(self.photoshop_instances.keys())[0]
return self.photoshop_instances[sid]
def get_client_info_from_event(self, sid, data):
ip = self.sid_to_ip[sid]
if not ip:
return
client_id = data.get('client_id', None)
if not client_id:
client_id = 0
user_id = data.get('user_id', None)
if not user_id:
user_id = 0
return ip, client_id, user_id
def registerSocketListeners(self):
sio = self.sio
@sio.event
async def connect(sid, environ):
ip = self.sid_to_ip[sid] = environ['REMOTE_ADDR']
qs = environ['QUERY_STRING']
qsobj = dict(x.split('=') for x in qs.split('&'))
if 'type' not in qsobj:
raise exceptions.ConnectionRefusedError('instance type missed in query')
elif qsobj['type'] == 'photoshop':
user_id = qsobj.get('user_id', None)
if not user_id:
user_id = 0
self.photoshop_instances[sid] = await PhotoshopManager.instance().new_ps_instance(self, sid, ip, user_id)
elif qsobj['type'] == 'comfyui':
client_id = qsobj.get('client_id', None)
if not client_id:
client_id = 0
user_id = qsobj.get('user_id', None)
if not user_id:
user_id = 0
self.comfyui_instances[sid] = client_id
PhotoshopManager.instance().instance_from_client_info(ip, client_id, user_id)
else:
raise exceptions.ConnectionRefusedError('unknown instance type ' + qsobj['type'])
# only emit by photoshop instance
@sio.event
async def disconnect(sid):
if sid in self.photoshop_instances:
instance = self.photoshop_instances.pop(sid, None)
instance.destroy()
elif sid in self.comfyui_instances:
self.comfyui_instances.pop(sid, None)
self.sid_to_ip.pop(sid, None)
# only emit by photoshop instance
@sio.event
async def sync_layers(sid, data):
instance = self.get_ps_instance(sid)
if instance is None:
return
instance.layers = data['layers']
layer_strs = instance.get_base_layers()
bounds_strs = instance.get_bounds_layers()
set_layer_strs = instance.get_set_layers()
for sid, client_id in self.comfyui_instances.items():
check_instance = PhotoshopManager.instance().instance_from_client_id(client_id)
if check_instance != instance:
continue
await sio.emit('sync_layers', {
'layer_strs': layer_strs,
'bound_strs': bounds_strs,
'set_layer_strs': set_layer_strs,
}, to=sid)
@sio.event
async def check_changes(sid, data):
ip, client_id, user_id = self.get_client_info_from_event(sid, data)
instance = PhotoshopManager.instance().instance_from_client_info(ip, client_id, user_id)
if instance is None:
return
if not await instance.is_ps_history_changed():
return
await sio.emit('trigger_graph_change', to=sid)
@sio.event
async def init(sid, data):
ip, client_id, user_id = self.get_client_info_from_event(sid, data)
instance = PhotoshopManager.instance().instance_from_client_info(ip, client_id, user_id)
if instance is None:
return
instance.reset_change_tracker()
+136
View File
@@ -0,0 +1,136 @@
import { app } from "../../../scripts/app.js";
import { api } from "../../../scripts/api.js"
import socketio from './socket.io.js'
let layerStrs = [];
let boundsStrs = [];
let setLayerStrs = [];
let socket = null;
console.log("[sd-ppp]", "Loading js extension");
const DEFAULT_USER_ID = "Change if sharing remote server"
function getUserId() {
let userId = app.ui.settings.getSettingValue("SD-PPP.userId");
if (!userId || userId == DEFAULT_USER_ID) {
userId = "";
}
return userId;
}
app.registerExtension({
name: "Comfy.SD-PPP",
init() {
},
async setup() {
// set change query loop
setInterval(checkChanges, 1000);
// add setting for using remote server
const userName = localStorage["Comfy.userName"];
const emptyValue = app.multiUserServer ? userName : DEFAULT_USER_ID
app.ui.settings.addSetting({
id: "SD-PPP.userId",
name: "SD-PPP: User ID",
defaultValue: emptyValue,
onChange: async (value, oldValue) => {
if (!value) {
setTimeout(() => {
app.ui.settings.setSettingValue("SD-PPP.userId", emptyValue);
if (!app.multiUserServer) return;
// Change empty text box to user name immediately when in multi-user mode, it's recommanded in multi-user environment
const input = document.querySelector("#SD-PPP-userId")
if (input) input.value = emptyValue;
}, 0.01);;
}
},
type: "text",
});
socket = socketio(location.origin, {
transports: ["websocket"],
path: '/sd-ppp/',
query: {
version: 1,
type: 'comfyui',
client_id: api.clientId,
user_id: getUserId(),
}
});
socket.on('connect', () => {
socket.emit('init', {
client_id: api.clientId,
user_id: getUserId(),
});
});
socket.on('disconnect', () => {
});
socket.on('connect_error', () => {
console.error('sdppp socket connect_error')
});
socket.on('sync_layers', (data) => {
layerStrs = data.layer_strs;
boundsStrs = data.bound_strs;
setLayerStrs = data.set_layer_strs;
});
socket.on('trigger_graph_change', () => {
api.dispatchEvent(new CustomEvent("graphChanged"));
});
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeType.comfyClass === 'Get Image From Photoshop Layer') {
const onSelected = nodeType.prototype.onSelected;
const onMouseEnter = nodeType.prototype.onMouseEnter;
let this_handler = function() {
this.widgets[0].options.values = layerStrs;
this.widgets[1].options.values = boundsStrs;
}
nodeType.prototype.onSelected = async function(...args) {
if(onSelected) await onSelected.call(this, ...args);
this_handler.call(this);
}
nodeType.prototype.onMouseEnter = async function(...args) {
if(onMouseEnter) await onMouseEnter.call(this, ...args);
this_handler.call(this);
}
} else if (nodeType.comfyClass === 'Send Images To Photoshop') {
const onSelected = nodeType.prototype.onSelected;
const onMouseEnter = nodeType.prototype.onMouseEnter;
let this_handler = function() {
this.widgets[0].options.values = setLayerStrs;
}
nodeType.prototype.onSelected = async function(...args) {
if(onSelected) await onSelected.call(this, ...args);
this_handler.call(this);
}
nodeType.prototype.onMouseEnter = async function(...args) {
if(onMouseEnter) await onMouseEnter.call(this, ...args);
this_handler.call(this);
}
}
}
});
const SDPPPNodes = [
'Get Image From Photoshop Layer',
'Send Images To Photoshop',
]
async function checkChanges() {
await checkHistoryChanges();
}
async function checkHistoryChanges() {
try {
const currentState = app.graph.serialize();
const mode0NodeTypes = currentState.nodes.filter(node => node.mode == 0).map(node => node.type);
const containsSDPPPNodes = mode0NodeTypes.some(nodeType => SDPPPNodes.includes(nodeType));
if (!containsSDPPPNodes) return;
socket.emit('check_changes', {
client_id: api.clientId,
user_id: getUserId(),
})
} catch (e) {
console.error("[sd-ppp]", "Failed to check changes", e);
}
}
+1366 -1
View File
File diff suppressed because one or more lines are too long
+12 -2
View File
@@ -9,6 +9,7 @@
"minVersion": "24.4.0"
},
"requiredPermissions": {
"enableUserInfo": true,
"network": {
"domains": "all"
},
@@ -39,19 +40,28 @@
},
"minimumSize": {
"width": 300,
"height": 100
"height": 160
},
"maximumSize": {
<<<<<<< HEAD
"width": 300,
"height": 500
=======
"width": 400,
"height": 160
>>>>>>> tl/develop_sio_multi_user
},
"preferredDockedSize": {
"width": 300,
"height": 100
"height": 160
},
"preferredFloatingSize": {
"width": 300,
<<<<<<< HEAD
"height": 500
=======
"height": 160
>>>>>>> tl/develop_sio_multi_user
},
"icons": [
{
+12 -2
View File
@@ -9,6 +9,7 @@
"minVersion": "24.4.0"
},
"requiredPermissions": {
"enableUserInfo": true,
"network": {
"domains": "all"
},
@@ -39,19 +40,28 @@
},
"minimumSize": {
"width": 300,
"height": 100
"height": 160
},
"maximumSize": {
<<<<<<< HEAD
"width": 300,
"height": 500
=======
"width": 400,
"height": 160
>>>>>>> tl/develop_sio_multi_user
},
"preferredDockedSize": {
"width": 300,
"height": 100
"height": 160
},
"preferredFloatingSize": {
"width": 300,
<<<<<<< HEAD
"height": 500
=======
"height": 160
>>>>>>> tl/develop_sio_multi_user
},
"icons": [
{
@@ -30,7 +30,7 @@ export class PanelController {
create() {
this[_root] = document.createElement("div");
this[_root].style.height = "100vh";
this[_root].style.height = "130vh";
this[_root].style.overflow = "auto";
this[_root].style.padding = "8px";
+4
View File
@@ -6,7 +6,11 @@ import Main from "./panels/Main.jsx";
import { entrypoints } from "uxp";
<<<<<<< HEAD
const demosController = new PanelController(() => <Main/>, {id: "sdppp-connect", menuItems: [
=======
const demosController = new PanelController(() => <Main/>, {id: "comfy-connect", menuItems: [
>>>>>>> tl/develop_sio_multi_user
// { id: "reload1", label: "Reload Plugin", enabled: true, checked: false, oninvoke: () => location.reload() },
// { id: "dialog1", label: "About this Plugin", enabled: true, checked: false, oninvoke: () => aboutController.run() },
] });
+104 -1
View File
@@ -1,3 +1,4 @@
<<<<<<< HEAD
import React from "react";
import { storage } from "uxp";
import { Model, ComfyConnection, TEXTS } from "../system/system.mjs";
@@ -137,4 +138,106 @@ export default class Main extends React.Component {
</>
)
}
}
}
=======
import React from "react";
import ComfyConnection from "../system/ComfyConnection";
import { storage, userInfo } from "uxp";
export default class Main extends React.Component {
state = {
comfyURL: '',
isConnected: false,
userId: userInfo.userId().slice(0, 10),
ui_ExpandMore: false,
}
componentDidMount() {
ComfyConnection.onConnectStateChange(() => {
const instance = ComfyConnection.instance
console.log('isConnected:', instance?.isConnected)
this.setState({
isConnected: instance?.isConnected,
comfyURL: instance ? instance.comfyURL : ''
})
});
storage.secureStorage.getItem('userId').then((value) => {
if (!value) return
this.setState({ userId: Buffer.from(value).toString() })
if (this.state.userId) {
console.log('userId:', this.state.userId)
}
})
storage.secureStorage.getItem('comfyURL').then((value) => {
if (!value) return
this.setState({ comfyURL: Buffer.from(value).toString() })
if (this.state.comfyURL) {
console.log('comfyURL:', this.state.comfyURL)
this.doConnectOrDisconnect()
}
})
}
doConnectOrDisconnect() {
if (!ComfyConnection.instance?.isConnected)
ComfyConnection.createInstance(this.state.comfyURL, this.state.userId);
else
ComfyConnection.instance.disconnect();
}
render() {
const expandMore = this.state.ui_ExpandMore;
return (
<>
<sp-textfield
id="url-bar"
label="ComfyURL"
onInput={(ev) => { this.state.comfyURL = ev.currentTarget.value }}
value={this.state.comfyURL}
placeholder="http://127.0.0.1:8188"
></sp-textfield>
<sp-divider />
<div id="connection-more" className={expandMore ? "expand-menu expand" : "expand-menu collapse"}>
<div class="expand-menu-title" onClick={() => { this.setState({ui_ExpandMore: !expandMore}) }}>
<sp-label>{expandMore ? "▼ more" : "▶ more"}</sp-label>
</div>
<div class="content">
<div class="input-row" id="user-id-bar">
<sp-label>user-name:</sp-label>
<div class="input-label">
<sp-label>{this.state.userId}</sp-label>
<sp-textfield
label="USER ID"
onInput={(ev) => {
const userID = ev.currentTarget.value;
if (!userID)
storage.secureStorage.removeItem('userId');
else {
storage.secureStorage.setItem('userId', this.state.userId);
this.setState({
userId: userID
})
}
}}
value={this.state.userId}
placeholder="User Name: Change if sharing remote server"
></sp-textfield>
</div>
</div>
</div>
</div>
<div className="button-box">
<sp-button
id="connect-btn"
variant="primary"
onClick={this.doConnectOrDisconnect.bind(this)}
>{this.state.isConnected ? 'disconnect' : 'connect'}</sp-button>
</div>
<sp-divider size="small"></sp-divider>
</>
)
}
}
>>>>>>> tl/develop_sio_multi_user
+66
View File
@@ -24,6 +24,7 @@
#url-bar {
width: 100%;
}
<<<<<<< HEAD
.status-bar {
display: flex;
justify-content: left;
@@ -36,6 +37,12 @@
margin: 0 5px;
}
.connect-box {
=======
#user-id-bar {
width: 100%;
}
.button-box {
>>>>>>> tl/develop_sio_multi_user
width: 100%;
display: flex;
justify-content: space-between;
@@ -44,6 +51,7 @@
}
#connect-btn {
text-align: center;
<<<<<<< HEAD
margin-right: 10px;
}
.client-list-item {
@@ -81,4 +89,62 @@
}
.autorun-desc {
font-size: 10px;
=======
}
.input-row {
display: flex;
flex-direction: row;
align-items: center;
height: 30px;
width: 100%;
}
.input-label {
position: relative;
flex: 1;
}
.input-label sp-label {
width: calc(100% - 15px);
height: 100%;
margin-left: 15px;
}
.input-label sp-textfield {
position: absolute;
width: 100%;
top: -5px;
left: 0;
visibility: hidden;
z-index: 5;
}
.input-label sp-textfield:focus {
visibility: visible;
}
.input-label:hover sp-textfield{
visibility: visible;
}
.expand-menu .expand-menu-title {
display: flex;
flex-direction: row;
align-items: center;
height: 15px;
padding: 5px;
}
.expand-menu .expand-menu-arrow {
font-weight: bold;
text-align: center;
}
.expand-menu .expand-menu-title sp-icon {
height: 15px;
width: 20px;
color: #ccc;
}
.expand-menu .content {
margin-left: 15px
}
.expand-menu.collapse .content {
display: none;
}
.expand-menu.expand .content {
display: flex;
>>>>>>> tl/develop_sio_multi_user
}
+124
View File
@@ -0,0 +1,124 @@
import { app } from "photoshop";
import { storage } from "uxp";
import { getAllSubLayer } from "./util";
import socketio from './library/socket.io.js'
import sendImages from "./events/send_images";
import getImage from "./events/get_image";
import getActiveHistoryStateId from "./events/get_active_history_state_id";
class ComfyConnection {
static instance = null;
static _connectStateCallbacks = [];
static onConnectStateChange(callback) {
ComfyConnection._connectStateCallbacks.push(callback);
}
static _callConnectStateChange() {
ComfyConnection._connectStateCallbacks.forEach(cb => {
try {
cb(ComfyConnection.instance?.isConnected)
} catch (e) { console.error(e) }
});
}
static createInstance(comfyURL, userId) {
if (ComfyConnection.instance && ComfyConnection.instance.isConnected) {
ComfyConnection.instance.disconnect();
}
ComfyConnection.instance = new ComfyConnection(comfyURL, userId);
}
get isConnected() {
return this.socket != null && this.socket.connected === true;
}
comfyURL = '';
interval = null;
constructor(comfyURL, userId) {
ComfyConnection.instance = this;
if (!comfyURL) {
comfyURL = 'http://127.0.0.1:8188';
}
this.comfyURL = comfyURL.replace(/\/*$/, '');
if (!userId) {
userId = '';
}
this.userId = userId;
this.connect();
}
connect() {
console.log('connect')
if (!this.socket) {
this._createSocket();
}
this.socket.connect();
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
}
}
_createSocket() {
console.log('create socket')
const socket = this.socket = socketio(this.comfyURL, {
autoConnect: false,
transports: ["websocket"],
path: '/sd-ppp/',
query: {
version: 1,
type: 'photoshop',
user_id: this.userId,
}
});
this.interval = setInterval(() => {
if (!this.isConnected) return;
const allLayers = getAllSubLayer(app.activeDocument);
this.socket.emit('sync_layers',
{ layers: allLayers }
)
}, 3000)
socket.on('connect_error', (error) => {
if (socket.active) {
console.error('connect_error reconnecting...')
} else {
console.error('connect_error disconnected')
}
ComfyConnection._callConnectStateChange();
});
socket.on('connect', () => {
console.log('connect')
storage.secureStorage.setItem('comfyURL', this.comfyURL);
ComfyConnection._callConnectStateChange();
});
socket.on('disconnect', () => {
console.log('disconnect')
ComfyConnection._callConnectStateChange();
});
socket.on('get_image', async (data, callback) => {
try {
const startTime = Date.now();
const result = await getImage(this.comfyURL, data)
console.log('get_image cost', Date.now() - startTime, 'ms');
callback(result)
} catch (e) { console.error(e); callback({ error: e.message }) }
})
socket.on('send_images', async (data, callback) => {
try {
const result = await sendImages(this.comfyURL, data)
callback(result)
} catch (e) { console.error(e); callback({ error: e.message }) }
})
socket.on('get_active_history_state_id', async (data, callback) => {
try {
const result = await getActiveHistoryStateId(data)
callback(result)
} catch (e) { console.error(e); callback({ error: e.message }) }
})
}
}
export default ComfyConnection;
+205
View File
@@ -0,0 +1,205 @@
import { app, imaging } from "photoshop";
import { executeAsModalUntilSuccess, findInAllSubLayer, unTrimImageData } from '../util.js';
import Jimp from "../library/jimp.min";
import { SPECIAL_LAYER_NAME_TO_ID, SPECIAL_LAYER_USE_SELECTION } from "../util";
function isLayerFolder(layer){
return layer.layers && layer.layers.length > 0;
}
async function findLayer(layerID) {
let layer;
let isFolder = false;
if (layerID <= 0) return [layer, isFolder];
layer = findInAllSubLayer(app.activeDocument, layerID)
if (!layer) throw new Error(`Layer(id: ${layerID}) not found`);
if (!isLayerFolder(layer)) return [layer, isFolder];
// layer is folder
const dupLayer = await layer.duplicate();
const mergedLayer = await dupLayer.merge()
isFolder = true;
return [mergedLayer, isFolder];
}
// ps returns trimmed data so need padding
function padAndTrimLayerDataToDesireBounds(layer, pixelDataFromAPI, desireBounds) {
if (pixelDataFromAPI.length == desireBounds.width * desireBounds.height * 4) {
return pixelDataFromAPI;
}
let pixelDataForReturn = new Uint8Array(desireBounds.width * desireBounds.height * 4);
let bounds = {
left: 0,
top: 0,
right: app.activeDocument.width,
bottom: app.activeDocument.height,
}
if (layer) bounds = layer.bounds;
unTrimImageData(
pixelDataFromAPI,
pixelDataForReturn,
bounds,
desireBounds
)
return pixelDataForReturn;
}
async function getPixelsDataHelper(layer, desireBounds) {
let options = {
documentID: app.activeDocument.id,
applyAlpha: false,
sourceBounds: desireBounds,
}
if (layer) options.layerID = layer.id
let pixels;
try {
pixels = await imaging.getPixels(options)
} catch (e) {
console.error(e.message)
return new Uint8Array(desireBounds.width * desireBounds.height * 4);
}
let psImageData = pixels.imageData
const pixelDataFromAPI = await psImageData.getData()
Promise.resolve().then(() => { psImageData.dispose() })
return pixelDataFromAPI
}
async function getPixelsData(layer, desireBounds) {
// layer null = document data
if (!layer) {
return await getPixelsDataHelper(null, desireBounds);
}
// normal layer
return await getPixelsDataHelper(layer, desireBounds);
}
function getDesiredBounds(boundsLayerID) {
const docBounds = {
left: 0,
top: 0,
right: app.activeDocument.width,
bottom: app.activeDocument.height,
width: app.activeDocument.width,
height: app.activeDocument.height
};
// use selection bounds
if (boundsLayerID == SPECIAL_LAYER_NAME_TO_ID[SPECIAL_LAYER_USE_SELECTION]) {
// if no selection use document bounds
const selectionBounds = app.activeDocument.selection?.bounds;
if (!selectionBounds) return docBounds;
return {
left: selectionBounds.left,
top: selectionBounds.top,
right: selectionBounds.right,
bottom: selectionBounds.bottom,
width: selectionBounds.width,
height: selectionBounds.height
}
}
let boundsLayer;
if (boundsLayerID > 0) {
boundsLayer = findInAllSubLayer(app.activeDocument, boundsLayerID)
if (!boundsLayer) throw new Error(`Bounds layer(id: ${boundsLayerID}) not found`);
}
// null boundsLayer = document bounds
if (!boundsLayer) return docBounds;
// empty boundsLayer = document bounds
const boundsLayerBounds = boundsLayer.bounds
const isEmptyBoundsLayer = boundsLayerBounds.left == 0 && boundsLayerBounds.top == 0 && boundsLayerBounds.right == 0 && boundsLayerBounds.bottom == 0;
if (isEmptyBoundsLayer) return docBounds;
// normal layer
let desireBounds = {
left: boundsLayerBounds.left,
top: boundsLayerBounds.top,
right: boundsLayerBounds.right,
bottom: boundsLayerBounds.bottom,
width: boundsLayerBounds.width,
height: boundsLayerBounds.height
};
return desireBounds
}
export default async function getImage(comfyURL, params) {
const layerID = params.layer_id
const boundsLayerID = params.use_layer_bounds
let uploadName = 0;
let layerOpacity = 100;
await executeAsModalUntilSuccess(async (executionContext) => {
let hostControl;
let suspensionID;
const startTime = Date.now();
let layer;
let isFolder = false;
const activeLayers = app.activeDocument?.activeLayers;
try {
hostControl = executionContext.hostControl;
suspensionID = await hostControl.suspendHistory({
"documentID": app.activeDocument.id,
"name": "Image To ComfyUI"
});
[layer, isFolder] = await findLayer(layerID);
layerOpacity = layer?.opacity ?? 100;
const desireBounds = getDesiredBounds(boundsLayerID);
const pixelDataFromAPI = await getPixelsData(layer, desireBounds)
const pixelDataForReturn = padAndTrimLayerDataToDesireBounds(layer, pixelDataFromAPI, desireBounds)
console.log('getPixels', Date.now() - startTime, 'ms');
// log desire size
const image = await new Promise((resolve, reject) => {
new Jimp({
data: pixelDataForReturn,
width: desireBounds.width,
height: desireBounds.height
}, (err, image) => {
err ? reject(err) : resolve(image);
})
})
console.log('new Jimp', Date.now() - startTime, 'ms');
image.quality(100);
console.log('quality', Date.now() - startTime, 'ms');
const file = await image.getBufferAsync(Jimp.MIME_PNG);
console.log('create pngfile', Date.now() - startTime, 'ms');
const fd = new FormData();
const PhotoshopBlob = new Blob([file], { type: "image/png" });
fd.append('image', PhotoshopBlob, "PhotoshopBlob.png") ;
fd.append('overwrite', "true");
console.log('start upload', Date.now() - startTime, 'ms');
const promise = fetch(comfyURL + '/upload/image', {
method: 'POST',
body: fd,
}).then(res => {
if (res.status == 200)
return res.json()
else
throw new Error('HTTP ' + res.status)
})
console.log('finish upload', Date.now() - startTime, 'ms')
const result = await promise
console.log('upload resulted', Date.now() - startTime, 'ms')
if (result.error) throw new Error(result.error);
if (!result.name) throw new Error('No upload_name')
uploadName = result.name
} catch (e) {
console.error(e);
throw e
} finally {
if (layer && isFolder) layer.delete();
if (activeLayers && activeLayers.length > 0) {
for (let i = 0; i < activeLayers.length; i++) {
activeLayers[i].selected = true;
}
}
if (hostControl && suspensionID) hostControl.resumeHistory(suspensionID);
}
}, { commandName: "get content of layer " + layerID })
return {
upload_name: uploadName,
layer_opacity: layerOpacity,
}
}
+108
View File
@@ -0,0 +1,108 @@
import { app, imaging } from "photoshop";
import { executeAsModalUntilSuccess} from '../util.js';
import Jimp from "../library/jimp.min";
import { SPECIAL_LAYER_NAME_TO_ID, SPECIAL_LAYER_NEW_LAYER } from '../util.js';
function autocrop(jimp) {
let minX = jimp.bitmap.width - 1;
let minY = jimp.bitmap.height - 1;
let maxX = 0;
let maxY = 0;
jimp.scan(0, 0, jimp.bitmap.width, jimp.bitmap.height, function(x, y, idx) {
const alpha = this.bitmap.data[idx + 3];
if (alpha !== 0) {
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
});
const width = maxX - minX + 1;
const height = maxY - minY + 1;
jimp.crop(minX, minY, width, height);
return jimp
}
export default async function sendImages(comfyURL, params) {
const imageIds = params.image_ids
const layerId = params.layer_id
await Promise.all(
imageIds.map(async imageId => {
let layer;
let existingLayerName;
let newLayerName;
await executeAsModalUntilSuccess(async () => {
try {
if (layerId && layerId != SPECIAL_LAYER_NAME_TO_ID[SPECIAL_LAYER_NEW_LAYER]) {
layer = await app.activeDocument.layers.find(l => l.id == layerId)
// deal with multiple images
let imageIndexSuffix = ""
console.log("imageIds.length: ", imageIds.length)
if (imageIds.length > 1){
index = imageIds.indexOf(imageId)
console.log("imageIds.index: ", index)
if (index > 0)
imageIndexSuffix = ` ${index}`
}
if (imageIndexSuffix != "" && layer != null){
const layerName = layer?.name;
existingLayerName = layerName + imageIndexSuffix
console.log("existingLayerName: ", existingLayerName)
layer = await app.activeDocument.layers.find(l => l.name == existingLayerName)
}
}
// deal with new layer or id/name not found layer
if (!layer) {
newLayerName = existingLayerName ?? 'Comfy Images ' + imageId
console.log("newLayerName: ", newLayerName)
layer = await app.activeDocument.createLayer("pixel", {
name: newLayerName
})
}
const jimp = (await Jimp.read(comfyURL + '/finished_images?id=' + imageId))
autocrop(jimp)
console.log("layer name ", layer.name)
let putPixelsOptions = {
layerID: layer.id,
imageData: await imaging.createImageDataFromBuffer(
jimp.bitmap.data,
{
width: jimp.bitmap.width,
height: jimp.bitmap.height,
components: 4,
colorSpace: "RGB"
}
),
replace: true,
}
if (!newLayerName) {
let bounds = layer.bounds
if (bounds.width != jimp.bitmap.width || bounds.height != jimp.bitmap.height) {
if (bounds.width <= 1 && bounds.height <= 1) {
bounds.left = jimp.bitmap.width / 2
bounds.top = jimp.bitmap.height / 2
}
let centerBounds = {}
centerBounds.left = bounds.left + (bounds.width - jimp.bitmap.width) / 2
centerBounds.top = bounds.top + (bounds.height - jimp.bitmap.height) / 2
centerBounds.right = bounds.left + jimp.bitmap.width
centerBounds.bottom = bounds.top + jimp.bitmap.height
centerBounds.width = jimp.bitmap.width
centerBounds.height = jimp.bitmap.height
bounds = centerBounds
}
putPixelsOptions.targetBounds = bounds
}
await imaging.putPixels(putPixelsOptions)
} catch(e) {
console.error(e)
throw e
}
})
})
)
return {}
}
+109
View File
@@ -0,0 +1,109 @@
import { core } from "photoshop";
export const SPECIAL_LAYER_USE_CANVAS = '### Use Canvas ###'
export const SPECIAL_LAYER_USE_SELECTION = '### Use Selection ###'
export const SPECIAL_LAYER_NEW_LAYER = '### New Layer ###'
export const SPECIAL_LAYER_SAME_AS_LAYER = '### Same as Layer ###'
export const SPECIAL_LAYER_NAME_TO_ID = {
[SPECIAL_LAYER_USE_CANVAS]: 0,
[SPECIAL_LAYER_USE_SELECTION]: -1,
[SPECIAL_LAYER_NEW_LAYER]: -2,
[SPECIAL_LAYER_SAME_AS_LAYER]: -3
}
export function unTrimImageData(
intersectImageDataArray,
toImageDataArray,
fromImageBounds,
toImageBounds
) {
const fromLeft = fromImageBounds.left;
const fromTop = fromImageBounds.top;
const fromRight = fromImageBounds.right;
const fromBottom = fromImageBounds.bottom;
const fromWidth = fromRight - fromLeft;
const fromHeight = fromBottom - fromTop;
const toLeft = toImageBounds.left;
const toTop = toImageBounds.top;
const toRight = toImageBounds.right;
const toBottom = toImageBounds.bottom;
const toWidth = toRight - toLeft;
const toHeight = toBottom - toTop;
const intersectLeft = Math.max(fromLeft, toLeft);
const intersectTop = Math.max(fromTop, toTop);
const intersectRight = Math.min(fromRight, toRight);
const intersectBottom = Math.min(fromBottom, toBottom);
const intersectWidth = intersectRight - intersectLeft;
const intersectHeight = intersectBottom - intersectTop;
const toLength = toWidth * toHeight * 4;
if (toImageDataArray.length !== toLength) {
throw new Error(`toImageDataArray.length(${toImageDataArray.length}) !== toLength(${toLength})`);
}
const intersectLength = intersectWidth * intersectHeight * 4;
if (intersectImageDataArray.length !== intersectLength) {
throw new Error(`fromImageDataArray.length(${intersectImageDataArray.length}) !== fromLength(${intersectLength})`);
}
for (let i = 0; i < toLength; i += 4) {
const currentLeft = (i / 4) % toWidth + toLeft;
const currentTop = Math.floor((i / 4) / toWidth) + toTop;
if (
currentLeft >= fromLeft &&
currentLeft < fromRight &&
currentTop >= fromTop &&
currentTop < fromBottom
) {
const fromIndex = ((currentTop - intersectTop) * intersectWidth + (currentLeft - intersectLeft)) * 4;
const alpha = intersectImageDataArray[fromIndex + 3];
toImageDataArray[i] = alpha == 0 ? 0 : intersectImageDataArray[fromIndex];
toImageDataArray[i + 1] = alpha == 0 ? 0 : intersectImageDataArray[fromIndex + 1];
toImageDataArray[i + 2] = alpha == 0 ? 0 : intersectImageDataArray[fromIndex + 2];
toImageDataArray[i + 3] = alpha;
}
}
return toImageDataArray;
}
export function getAllSubLayer(layer, level = 0) {
if (!layer?.layers) return [];
return layer.layers.reduce((ret, layer) => {
ret.push({
name: '-'.repeat(level) + layer.name,
id: layer.id
});
return ret.concat(getAllSubLayer(layer, level + 1));
}, []);
}
export function findInAllSubLayer(layer, layerid) {
if (!layer?.layers) return null;
for (let i = 0; i < layer.layers.length; i++) {
if (layer.layers[i].id === layerid) return layer.layers[i];
const result = findInAllSubLayer(layer.layers[i], layerid)
if (result) return result;
}
return null;
}
const executeAsModal = core.executeAsModal;
export async function executeAsModalUntilSuccess(...args) {
let result;
let failed = true;
while(failed) {
try {
result = await executeAsModal(...args);
failed = false;
} catch (e) {
if (e.number != 9) {
failed = false; // This case is hit if the targetFunction throws an exception
}
}
await new Promise(r => setTimeout(r, 200));
}
return result;
}
+166
View File
@@ -1,3 +1,4 @@
<<<<<<< HEAD:sdppp_python/comfy_nodes_legacy.py
import numpy as np
from nodes import LoadImage
from PIL import Image
@@ -198,3 +199,168 @@ def define_comfyui_nodes_legacy(sdppp):
'ImageTimesOpacity': ImageTimesOpacity,
'MaskTimesOpacity': MaskTimesOpacity,
}
=======
import numpy as np
import time
from nodes import LoadImage
from .apis import cache_images
from .photoshop_manager import PhotoshopManager
from server import PromptServer
prompt_server = PromptServer.instance
def define_comfyui_nodes(sdppp):
def call_async_func_in_server_thread(coro, dontwait = False):
handle = {
'done': False,
'result': None
}
loop = sdppp.loop
async def do_call():
handle['result'] = await coro
handle['done'] = True
loop.call_soon(lambda: loop.create_task(do_call()))
if not dontwait:
while not handle['done']:
pass
return handle['result']
else:
return None
class GetImageFromPhotoshopLayerNode:
# !!!!do not validate because there's no client_id when validating, so we validate manually before execution!!!!
# @classmethod
# def VALIDATE_INPUTS():
# pass
@classmethod
def IS_CHANGED(self, layer, use_layer_bounds):
photoshopInstance = PhotoshopManager.instance().instance_from_client_id(prompt_server.client_id)
if not photoshopInstance:
return np.random.rand()
else:
id = photoshopInstance.layer_name_to_id(layer)
bounds_id = photoshopInstance.layer_name_to_id(use_layer_bounds, id)
start = time.time()
is_changed, history_state_id = call_async_func_in_server_thread(photoshopInstance.check_layer_bounds_combo_changed(id, bounds_id))
if is_changed and history_state_id is None:
return photoshopInstance.update_comfyui_last_value(id, bounds_id, np.random.rand())
return photoshopInstance.update_comfyui_last_value(id, bounds_id, history_state_id)
@classmethod
def INPUT_TYPES(cls):
layer_strs = []
bounds_strs = []
return {
"required": {},
"optional": {
"layer": (layer_strs, {"default": layer_strs[0] if len(layer_strs) > 0 else None}),
"use_layer_bounds": (bounds_strs, {"default": bounds_strs[0] if len(bounds_strs) > 0 else None}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "FLOAT")
RETURN_NAMES = ("image_out", "mask_out", "layer_opacity")
FUNCTION = "get_image"
CATEGORY = "Photoshop"
def get_image(self, layer, use_layer_bounds):
photoshopInstance = PhotoshopManager.instance().instance_from_client_id(prompt_server.client_id)
if not photoshopInstance:
raise ValueError('Photoshop is not connected')
layer_strs = photoshopInstance.get_base_layers()
if layer not in layer_strs:
raise ValueError(f"Layer {layer} not found in Photoshop")
bounds_strs = photoshopInstance.get_bounds_layers()
if use_layer_bounds not in bounds_strs:
raise ValueError(f"Layer {use_layer_bounds} not found in Photoshop")
id = photoshopInstance.layer_name_to_id(layer)
bounds_id = photoshopInstance.layer_name_to_id(use_layer_bounds, id)
start = time.time()
image_id, layer_opacity = call_async_func_in_server_thread(photoshopInstance.get_image(layer_id=id, bounds_id=bounds_id))
loadImage = LoadImage()
(output_image, output_mask) = loadImage.load_image(image_id)
return (output_image, output_mask, layer_opacity / 100)
class SendImageToPhotoshopLayerNode:
# !!!!do not validate because there's no client_id when validating, so we validate manually before execution!!!!
# @classmethod
# def VALIDATE_INPUTS():
# pass
@classmethod
def INPUT_TYPES(cls):
layer_strs = []
return {
"required": {},
"optional": {
"images": ("IMAGE", ),
"layer": (layer_strs, {"default": layer_strs[0] if len(layer_strs) > 0 else None}),
}
}
RETURN_TYPES = ()
FUNCTION = "send_image"
CATEGORY = "Photoshop"
OUTPUT_NODE = True
def send_image(self, images, layer):
photoshopInstance = PhotoshopManager.instance().instance_from_client_id(prompt_server.client_id)
if not photoshopInstance:
raise ValueError('Photoshop is not connected')
ret = cache_images(images)
layer_id = photoshopInstance.layer_name_to_id(layer)
call_async_func_in_server_thread(photoshopInstance.send_images(image_ids=ret, layer_id=layer_id), True)
return (None,)
class ImageTimesOpacity:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE", ),
"opacity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "image_times_opacity"
CATEGORY = "Photoshop"
def image_times_opacity(self, images, opacity):
image_out = images * opacity
return (image_out,)
class MaskTimesOpacity:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"masks": ("MASK", ),
"opacity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}),
}
}
RETURN_TYPES = ("MASK",)
FUNCTION = "mask_times_opacity"
CATEGORY = "Photoshop"
def mask_times_opacity(self, masks, opacity):
mask_out = masks * opacity
return (mask_out,)
return {
'GetImageFromPhotoshopLayerNode': GetImageFromPhotoshopLayerNode,
'SendImageToPhotoshopLayerNode': SendImageToPhotoshopLayerNode,
'ImageTimesOpacity': ImageTimesOpacity,
'MaskTimesOpacity': MaskTimesOpacity,
}
>>>>>>> tl/develop_sio_multi_user:comfy/nodes.py
+116
View File
@@ -0,0 +1,116 @@
import asyncio
from .photoshop_instance import PhotoshopInstance
DEFAULT_ID = 0
class PhotoshopManager:
_instance = None
@classmethod
def instance(cls) -> 'PhotoshopManager':
if cls._instance is None:
cls._instance = PhotoshopManager()
return cls._instance
def __init__(self):
# client id info
self.ip_to_client_id_list = {}
self.client_id_to_userid = {}
# ps instance info
self.ip_to_ps_instance_list = {}
# final match
self.client_id_to_ps_instance = {}
# get instance from client information, match new instance if not exist
def instance_from_client_info(self, ip, client_id, user_id) -> PhotoshopInstance:
if not ip: return None
if not client_id: user_id = 0
if not user_id: user_id = 0
instance = self.instance_from_client_id(client_id)
if instance is not None:
return instance
self._add_new_client(ip, client_id, user_id)
self._client_match_ps(ip, client_id, user_id)
return self.instance_from_client_id(client_id)
# get instance from client id
def instance_from_client_id(self, client_id) -> PhotoshopInstance:
return self.client_id_to_ps_instance.get(client_id, None)
# create new instance and record
async def new_ps_instance(self, sdppp, sid, ip, user_id) -> PhotoshopInstance:
if not ip: return None
if not user_id: user_id = 0
# create new instance
instance = PhotoshopInstance(sdppp, sid, user_id)
instance.on_destroy = self._on_instance_destroy
# record new instance
await self._add_new_ps_instance(ip, instance)
return instance
#------------------------------------------------------------------------------------------------------------------------------------------------
# callback when instance is destroyed and remove from record
def _on_instance_destroy(self, instance):
self._remove_instance(instance)
# record new ps instance for matching
async def _add_new_ps_instance(self, ip, instance):
# find old one and destroy
same_ip_list = self.ip_to_ps_instance_list.get(ip, [])
for check_instance in same_ip_list:
if check_instance.uid == instance.uid:
await check_instance.destroy()
# record ip to instance
ps_instance_list = self.ip_to_ps_instance_list.get(ip, [])
if instance not in ps_instance_list:
ps_instance_list.append(instance)
self.ip_to_ps_instance_list[ip] = ps_instance_list
# record new client id
def _add_new_client(self, ip, client_id, user_id):
# record client id, if exist, do nothing
client_id_list = self.ip_to_client_id_list.get(ip, [])
if client_id in client_id_list:
return
client_id_list.append(client_id)
self.ip_to_client_id_list[ip] = client_id_list
# record user_id
self.client_id_to_userid[client_id] = user_id
# match client id to ps instance for prompt use
# there are several ways to match:
# 1. one to one match(local network most likely): same ip client and ps instance, assign to it
# 2. multiple ps instance under the same ip(remote service most likely), check user id
def _client_match_ps(self, ip, client_id, user_id):
# check existing pair
instance = self.client_id_to_ps_instance.get(client_id, None)
if instance is not None:
return
# check same ip ps instance
ps_instance_list = self.ip_to_ps_instance_list.get(ip, [])
count = len(ps_instance_list)
if count <= 0:
return
# if only one same ip ps instance, assign client id
if count == 1:
self.client_id_to_ps_instance[client_id] = ps_instance_list[0]
return
# if more than one same ip ps instance, check same user_id, same ip + same user id = same ps instance
ps_user_id_list = [instance.uid for instance in ps_instance_list]
try:
check_index = ps_user_id_list.index(user_id)
except:
check_index = None
if check_index is None:
return
self.client_id_to_ps_instance[client_id] = ps_instance_list[check_index]
def _remove_instance(self, instance):
# remove id from ip to instance list
for ip, ps_instance_list in self.ip_to_ps_instance_list.items():
if instance in ps_instance_list:
ps_instance_list.remove(instance)
# remove instance from client id to instance
for client_id in list(self.client_id_to_ps_instance.keys()):
check_instance = self.client_id_to_ps_instance.get(client_id, None)
if check_instance == instance:
self.client_id_to_ps_instance.pop(client_id, None)