Skip to content

Commit 16fcb17

Browse files
committed
Support FORM post request, Add Chainable Image, and Image Converters (Base64 / Blob)
1 parent cfaea2a commit 16fcb17

14 files changed

+377
-4
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ ComfyUI-RequestNodes is a custom node plugin for ComfyUI that provides functiona
88

99
* **Get Request Node**: Sends GET requests and retrieves responses.
1010
* **Post Request Node**: Sends POST requests and retrieves responses.
11+
* **Form Post Request Node**: Sends POST requests in `multipart/form-data` format, supporting file (image) uploads.
1112
* **Rest Api Node**: A versatile node for sending various HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) with retry settings.
13+
* **Image to Base64 Node**: Converts an image to a Base64 encoded string.
14+
* **Image to Blob Node**: Converts an image to a Blob (Binary Large Object).
1215
* **Key/Value Node**: Creates key/value pairs for building request parameters, headers, or other dictionary-like structures.
16+
* **Chain Image Node**: Uploads an image and adds it to an image batch, allowing for chaining to build a batch from multiple images.
1317
* **String Replace Node**: Replaces placeholders in a string with provided values.
1418
* **Retry Settings Node**: Creates retry setting configurations for the Rest Api Node.
1519

@@ -21,6 +25,8 @@ The plugin includes the following test resources:
2125
![rest_node](workflows/get_node.png)
2226
* `post_node.json` - POST request workflow template
2327
![rest_node](workflows/post_node.png)
28+
* `form-post-request-node.json` - FORM POST request workflow template
29+
![rest_node](workflows/form-post-request-node.png)
2430
* `workflows/rest_node.json` - REST API request workflow template
2531
![rest_node](workflows/rest_node.png)
2632

@@ -73,6 +79,33 @@ After installation, you can find the nodes under the "RequestNode" category in t
7379
* `any` (ANY): The raw response content.
7480
* ![image](https://github.com/user-attachments/assets/6eda9fef-48cf-478c-875e-6bd6d850bff2)
7581

82+
* **Form Post Request Node**:
83+
* **Category**: RequestNode/Post Request
84+
* **Inputs**:
85+
* `target_url` (STRING, required): The URL to send the POST request to.
86+
* `image` (IMAGE, required): The image or image batch to upload. If an image batch is provided, all images will be sent in the same request.
87+
* `image_field_name` (STRING, required): The field name for the image in the form.
88+
* `form_fields` (KEY_VALUE, optional): Other form fields, typically from a Key/Value Node.
89+
* `headers` (KEY_VALUE, optional): Request headers, typically from a Key/Value Node.
90+
* **Outputs**:
91+
* `text` (STRING): The response body as text.
92+
* `json` (JSON): The response body parsed as JSON (if valid).
93+
* `any` (ANY): The raw response content.
94+
95+
* **Image to Base64 Node**:
96+
* **Category**: RequestNode/Converters
97+
* **Inputs**:
98+
* `image` (IMAGE, required): The image to convert.
99+
* **Outputs**:
100+
* `STRING`: The Base64 encoded image string.
101+
102+
* **Image to Blob Node**:
103+
* **Category**: RequestNode/Converters
104+
* **Inputs**:
105+
* `image` (IMAGE, required): The image to convert.
106+
* **Outputs**:
107+
* `BYTES`: The raw binary data of the image.
108+
76109
* **Rest Api Node**:
77110
* **Category**: RequestNode/REST API
78111
* **Inputs**:
@@ -99,6 +132,17 @@ After installation, you can find the nodes under the "RequestNode" category in t
99132
* `KEY_VALUE` (KEY_VALUE): A dictionary containing the key/value pair(s).
100133
* ![image](https://github.com/user-attachments/assets/dfe7dab0-2b1b-4f99-ac6f-89e01d03b7e0)
101134

135+
* **Chain Image Node**:
136+
* **Category**: RequestNode/Utils
137+
* **Description**: This node allows you to upload an image and add it to an image batch. You can chain multiple nodes of this type together to create a batch of several images. If you upload images with different dimensions, this node will automatically resize subsequent images to match the dimensions of the first one in the batch.
138+
* **Inputs**:
139+
* `image` (IMAGE, required): The image to upload. Use the "choose file to upload" button.
140+
* `image_batch_in` (IMAGE, optional): An existing image batch to append the newly uploaded image to. This can be connected from another `Chain Image Node` to build a batch.
141+
* **Outputs**:
142+
* `image_batch_out` (IMAGE): The combined image batch.
143+
* **Example**:
144+
* ![image](workflows/chainable_upload_image_node.png)
145+
102146
* **String Replace Node**:
103147
* **Category**: RequestNode/Utils
104148
* **Inputs**:

README_zh.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ ComfyUI-RequestNodes 是一個用於 ComfyUI 的自訂節點插件,提供了
88

99
* **Get Request Node**: 發送 GET 請求並檢索響應。
1010
* **Post Request Node**: 發送 POST 請求並檢索響應。
11+
* **Form Post Request Node**: 發送 `multipart/form-data` 格式的 POST 請求,支援檔案(圖片)上傳。
1112
* **Rest Api Node**: 一個多功能的節點,用於發送各種 HTTP 方法 (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS) 並支援重試設定。
13+
* **Image to Base64 Node**: 將圖片轉換為 Base64 編碼的字串。
14+
* **Image to Blob Node**: 將圖片轉換為 Blob (二進位大型物件)。
1215
* **Key/Value Node**: 創建鍵/值對,用於構建請求參數、標頭或其他類似字典的結構。
16+
* **Chain Image Node**: 上傳圖片並將其添加到圖片批次中,支援鏈式操作以構建多圖批次。
1317
* **String Replace Node**: 使用提供的值替換字串中的佔位符。
1418
* **Retry Settings Node**: 為 Rest Api Node 創建重試設定配置。
1519

@@ -21,6 +25,8 @@ ComfyUI-RequestNodes 是一個用於 ComfyUI 的自訂節點插件,提供了
2125
![rest_node](workflows/get_node.png)
2226
* `post_node.json` - POST 請求工作流程模板
2327
![rest_node](workflows/post_node.png)
28+
* `form-post-request-node.json` - FORM POST 請求工作流程模板
29+
![rest_node](workflows/form-post-request-node.png)
2430
* `workflows/rest_node.json` - REST API 請求工作流程模板
2531
![rest_node](workflows/rest_node.png)
2632

@@ -73,6 +79,33 @@ ComfyUI-RequestNodes 是一個用於 ComfyUI 的自訂節點插件,提供了
7379
* `any` (ANY): 原始響應內容。
7480
* ![image](https://github.com/user-attachments/assets/6eda9fef-48cf-478c-875e-6bd6d850bff2)
7581

82+
* **Form Post Request Node**:
83+
* **分類**: RequestNode/Post Request
84+
* **輸入**:
85+
* `target_url` (STRING, 必需): 要發送 POST 請求的 URL。
86+
* `image` (IMAGE, 必需): 要上傳的圖片或圖片批次。如果提供圖片批次,所有圖片將在同一個請求中發送。
87+
* `image_field_name` (STRING, 必需): 圖片在表單中的欄位名稱。
88+
* `form_fields` (KEY_VALUE, 可選): 其他表單欄位,通常來自 Key/Value Node。
89+
* `headers` (KEY_VALUE, 可選): 請求標頭,通常來自 Key/Value Node。
90+
* **輸出**:
91+
* `text` (STRING): 響應主體作為文本。
92+
* `json` (JSON): 響應主體解析為 JSON (如果有效)。
93+
* `any` (ANY): 原始響應內容。
94+
95+
* **Image to Base64 Node**:
96+
* **分類**: RequestNode/Converters
97+
* **輸入**:
98+
* `image` (IMAGE, 必需): 要轉換的圖片。
99+
* **輸出**:
100+
* `STRING`: Base64 編碼的圖片字串。
101+
102+
* **Image to Blob Node**:
103+
* **分類**: RequestNode/Converters
104+
* **輸入**:
105+
* `image` (IMAGE, 必需): 要轉換的圖片。
106+
* **輸出**:
107+
* `BYTES`: 圖片的原始二進位資料。
108+
76109
* **Rest Api Node**:
77110
* **分類**: RequestNode/REST API
78111
* **輸入**:
@@ -99,6 +132,17 @@ ComfyUI-RequestNodes 是一個用於 ComfyUI 的自訂節點插件,提供了
99132
* `KEY_VALUE` (KEY_VALUE): 包含鍵值對的字典。
100133
* ![image](https://github.com/user-attachments/assets/dfe7dab0-2b1b-4f99-ac6f-89e01d03b7e0)
101134

135+
* **Chain Image Node**:
136+
* **分類**: RequestNode/Utils
137+
* **說明**: 此節點允許您上傳一張圖片並將其添加到圖片批次中。您可以將多個此類型的節點鏈接在一起,以創建包含多張圖片的批次。如果您上傳的圖片尺寸不同,此節點會自動將後續圖片的大小調整為與批次中第一張圖片的尺寸相符。
138+
* **輸入**:
139+
* `image` (IMAGE, 必需): 要上傳的圖片。請使用 "choose file to upload" 按鈕。
140+
* `image_batch_in` (IMAGE, 可選): 一個已有的圖片批次,用於附加新上傳的圖片。可連接另一個 `Chain Image Node` 的輸出來構建批次。
141+
* **輸出**:
142+
* `image_batch_out` (IMAGE): 合併後的圖片批次。
143+
* **範例**:
144+
* ![image](workflows/chainable_upload_image_node.png)
145+
102146
* **String Replace Node**:
103147
* **分類**: RequestNode/Utils
104148
* **輸入**:

__init__.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,22 @@
66
from .rest_api_node import RestApiNode
77
from .string_replace_node import StringReplaceNode
88
from .retry_setting_node import RetrySettingNode
9+
from .form_post_node import FormPostRequestNode
10+
from .image_to_base64_node import ImageToBase64Node
11+
from .image_to_blob_node import ImageToBlobNode
12+
from .image_list_combiner_node import ChainableUploadImage
913

1014

11-
NODE_CLASS_MAPPINGS = {
15+
16+
NODE_CLASS_MAPPINGS = {
1217
"Get Request Node": GetRequestNode,
1318
"Post Request Node": PostRequestNode,
19+
"Form Post Request Node": FormPostRequestNode,
1420
"Rest Api Node": RestApiNode,
1521
"Key/Value Node": KeyValueNode,
1622
"String Replace Node": StringReplaceNode,
17-
"Retry Settings Node": RetrySettingNode
18-
}
23+
"Retry Settings Node": RetrySettingNode,
24+
"Image To Base64 Node": ImageToBase64Node,
25+
"Image To Blob Node": ImageToBlobNode,
26+
"Chainable Upload Image": ChainableUploadImage,
27+
}

base_flask_server.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import traceback
77
from werkzeug.exceptions import HTTPException
88
import random
9+
import os
910

1011
app = Flask(__name__)
1112
CORS(app)
@@ -150,6 +151,40 @@ def return_request_info():
150151
print(f"处理 /api/request_info 请求时出错: {str(e)}")
151152
return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500
152153

154+
@app.route('/api/form_test', methods=['POST'])
155+
def handle_form_post():
156+
print(f"Received POST request on /api/form_test")
157+
try:
158+
form_data = request.form.to_dict()
159+
files_info = {}
160+
if request.files:
161+
upload_folder = 'uploads'
162+
if not os.path.exists(upload_folder):
163+
os.makedirs(upload_folder)
164+
165+
for field_name in request.files:
166+
files_info[field_name] = []
167+
for file_storage in request.files.getlist(field_name):
168+
filename = file_storage.filename
169+
file_path = os.path.join(upload_folder, filename)
170+
file_storage.save(file_path)
171+
172+
files_info[field_name].append({
173+
"filename": filename,
174+
"content_type": file_storage.content_type,
175+
"saved_path": file_path
176+
})
177+
178+
response_data = {
179+
"received_form_data": form_data,
180+
"received_files_info": files_info
181+
}
182+
print(f"Form test data: {response_data}")
183+
return jsonify(response_data)
184+
except Exception as e:
185+
print(f"处理 /api/form_test 请求时出错: {str(e)}")
186+
return jsonify({"error": "处理请求时发生错误", "details": str(e)}), 500
187+
153188

154189
if __name__ == "__main__":
155190
print("Starting RestApiNode test server...")

form_post_node.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import requests
2+
import json
3+
import io
4+
import torch
5+
import numpy as np
6+
from PIL import Image
7+
8+
class FormPostRequestNode:
9+
def __init__(self):
10+
pass
11+
12+
@classmethod
13+
def INPUT_TYPES(s):
14+
return {
15+
"required": {
16+
"target_url": ("STRING", {"default": "http://127.0.0.1:7788/api/echo"}),
17+
"image": ("IMAGE",),
18+
"image_field_name": ("STRING", {"default": "image"}),
19+
},
20+
"optional": {
21+
"form_fields": ("KEY_VALUE",),
22+
"headers": ("KEY_VALUE",),
23+
}
24+
}
25+
26+
RETURN_TYPES = ("STRING", "JSON", "ANY")
27+
RETURN_NAMES = ("text", "json", "any")
28+
29+
FUNCTION = "make_form_post_request"
30+
31+
CATEGORY = "RequestNode/Post Request"
32+
33+
def make_form_post_request(self, target_url, image, image_field_name, form_fields=None, headers=None):
34+
files = []
35+
for i, single_image in enumerate(image):
36+
# Convert tensor to PIL Image
37+
img_np = 255. * single_image.cpu().numpy()
38+
img = Image.fromarray(np.clip(img_np, 0, 255).astype(np.uint8))
39+
40+
# Save image to a byte buffer
41+
buffer = io.BytesIO()
42+
img.save(buffer, format='PNG')
43+
buffer.seek(0)
44+
45+
files.append((image_field_name, (f'image_{i}.png', buffer, 'image/png')))
46+
47+
data = form_fields if form_fields else {}
48+
49+
request_headers = {}
50+
if headers:
51+
request_headers.update(headers)
52+
53+
try:
54+
response = requests.post(target_url, files=files, data=data, headers=request_headers)
55+
56+
text_output = response.text
57+
58+
try:
59+
json_output = response.json()
60+
except json.JSONDecodeError:
61+
json_output = {"error": "Response is not valid JSON"}
62+
63+
any_output = response.content
64+
65+
except Exception as e:
66+
error_message = str(e)
67+
text_output = error_message
68+
json_output = {"error": error_message}
69+
any_output = error_message.encode()
70+
71+
return (text_output, json_output, any_output)
72+
73+
NODE_CLASS_MAPPINGS = {
74+
"FormPostRequestNode": FormPostRequestNode
75+
}
76+
77+
NODE_DISPLAY_NAME_MAPPINGS = {
78+
"FormPostRequestNode": "Form Post Request Node"
79+
}

image_list_combiner_node.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import torch
2+
import time
3+
import torch.nn.functional as F
4+
5+
class ChainableUploadImage:
6+
"""
7+
A node that allows uploading an image and adding it to an image batch.
8+
It can be chained with other nodes of the same type to build a batch of images,
9+
similar to the default 'Load Image' node but with chaining capability.
10+
"""
11+
@classmethod
12+
def INPUT_TYPES(s):
13+
return {
14+
"required": {
15+
# This widget tells the frontend to show an upload button
16+
"image": ("IMAGE", {"image_upload": True})
17+
},
18+
"optional": {
19+
"image_batch_in": ("IMAGE",),
20+
}
21+
}
22+
23+
RETURN_TYPES = ("IMAGE",)
24+
RETURN_NAMES = ("image_batch_out",)
25+
FUNCTION = "load_and_chain"
26+
CATEGORY = "RequestNode/Utils"
27+
28+
def load_and_chain(self, image, image_batch_in=None):
29+
# The 'image' parameter from the upload widget is already a tensor.
30+
# No file loading or conversion is needed.
31+
32+
# 'image' is a tensor of shape [1, H, W, C]
33+
if image_batch_in is None:
34+
# Start a new batch with the uploaded image.
35+
return (image,)
36+
else:
37+
# Get the dimensions of the incoming batch
38+
target_h = image_batch_in.shape[1]
39+
target_w = image_batch_in.shape[2]
40+
41+
# Get the dimensions of the new image
42+
current_h = image.shape[1]
43+
current_w = image.shape[2]
44+
45+
# Resize if dimensions don't match
46+
if current_h != target_h or current_w != target_w:
47+
# Permute from [B, H, W, C] to [B, C, H, W] for interpolation
48+
image_permuted = image.permute(0, 3, 1, 2)
49+
# Resize
50+
resized_image_permuted = F.interpolate(
51+
image_permuted,
52+
size=(target_h, target_w),
53+
mode='bilinear',
54+
align_corners=False
55+
)
56+
# Permute back to [B, H, W, C]
57+
image = resized_image_permuted.permute(0, 2, 3, 1)
58+
59+
# Concatenate the incoming batch with the new (potentially resized) image tensor.
60+
combined_tensor = torch.cat((image_batch_in, image), dim=0)
61+
return (combined_tensor,)
62+
63+
@classmethod
64+
def IS_CHANGED(s, image, image_batch_in=None):
65+
# Since the image is an uploaded tensor, we can't hash a file.
66+
# We'll return the current time to ensure the node re-executes
67+
# whenever a new image is uploaded.
68+
return time.time()
69+
70+
# For ComfyUI to discover this node
71+
NODE_CLASS_MAPPINGS = {
72+
"ChainableUploadImage": ChainableUploadImage
73+
}
74+
75+
# A friendly name for the node in the UI
76+
NODE_DISPLAY_NAME_MAPPINGS = {
77+
"ChainableUploadImage": "Upload and Chain Image"
78+
}

0 commit comments

Comments
 (0)