├── .github └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_en.md ├── __init__.py ├── nodes ├── base_compressor.py ├── batch_image_compressor.py └── image_compressor.py ├── pyproject.toml ├── requirements.txt └── workflow.png /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | paths: 9 | - "pyproject.toml" 10 | 11 | permissions: 12 | issues: write 13 | 14 | jobs: 15 | publish-node: 16 | name: Publish Custom Node to registry 17 | runs-on: ubuntu-latest 18 | if: ${{ github.repository_owner == 'liuqianhonga' }} 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v4 22 | - name: Publish Custom Node 23 | uses: Comfy-Org/publish-node-action@v1 24 | with: 25 | ## Add your own personal access token to your Github Repository secrets and reference it here. 26 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 liuqianhong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Image Compressor Node 2 | 3 | [English](README_en.md) | 中文 4 | 5 | 一个用于图像压缩的ComfyUI自定义节点,支持多种压缩格式和参数调整。 6 | 7 | ![image](workflow.png) 8 | 9 | > 直接拖入ComfyUI 10 | 11 | ## 功能特点 12 | 13 | - 支持多种图像格式(PNG、WEBP、JPEG)的压缩 14 | - 可调整图像质量和压缩级别 15 | - 支持图像尺寸调整 16 | - 自定义输出路径和文件名前缀 17 | - 显示压缩前后的文件大小 18 | - 支持批量图像处理 19 | - 自动处理透明通道 20 | - 智能处理多种输入格式(RGB/RGBA) 21 | 22 | ## 安装方法 23 | 24 | 1. 将本插件复制到 ComfyUI 的 `custom_nodes` 目录下 25 | 2. 重启 ComfyUI 26 | 3. 在节点列表中找到压缩节点: 27 | - 路径: image/Image Compressor (单图压缩节点) 28 | - 路径: image/Batch Image Compressor (批量压缩节点) 29 | 30 | ## 节点类型 31 | 32 | ### 单图压缩节点 (Image Compressor) 33 | 34 | #### 输入参数 35 | - **images**: 输入图像(支持单张或批量图像) 36 | - **format**: 输出格式选择(PNG/WEBP/JPEG) 37 | - **quality**: 压缩质量(1-100) 38 | - **resize_factor**: 尺寸调整因子(0.1-1.0) 39 | - **compression_level**: 压缩级别(0-9,仅PNG格式有效) 40 | - **save_image**: 是否保存压缩后的图像文件(默认:是) 41 | - **output_prefix**: 输出文件名前缀(默认:compressed_) 42 | - **output_path**: 自定义输出路径 43 | - 绝对路径:直接使用该路径 44 | - 例如:`D:/my_images/compressed` 45 | - 相对路径:基于 ComfyUI 的 output 目录 46 | - 例如:`my_folder` → `output/my_folder` 47 | - 例如:`compressed/png` → `output/compressed/png` 48 | - 留空:默认使用 output/compressed 目录 49 | 50 | #### 输出信息 51 | - **compression_info**: 压缩信息字符串,包含: 52 | - 压缩后的文件大小 53 | - 原始文件大小 54 | - 保存路径(如果启用了保存) 55 | - 压缩比率信息 56 | - **images**: 压缩后的图像 57 | - 可以直接连接到其它需要图像输入的节点 58 | - 保持原始批次结构 59 | - 包含所有压缩处理后的图像数据 60 | 61 | ### 批量压缩节点 (Batch Image Compressor) 62 | 63 | #### 输入参数 64 | - **input_path**: 输入图像目录路径 65 | - 支持处理目录下的所有图像文件 66 | - 自动识别 PNG、JPEG、WEBP 格式 67 | - 会递归处理子目录中的图像 68 | - **save_image**: 是否保存压缩后的图像文件(默认:是) 69 | - **output_prefix**: 输出文件名前缀(默认:compressed_) 70 | - **output_path**: 自定义输出路径 71 | - 绝对路径:直接使用该路径 72 | - 例如:`D:/my_images/compressed` 73 | - 相对路径:基于 ComfyUI 的 output 目录 74 | - 例如:`my_folder` → `output/my_folder` 75 | - 例如:`compressed/png` → `output/compressed/png` 76 | - 留空:默认使用 output/compressed 目录 77 | 78 | #### 输出信息 79 | - **compression_info**: 压缩信息字符串,包含: 80 | - 压缩后的文件大小 81 | - 原始文件大小 82 | - 保存路径(如果启用了保存) 83 | - 压缩比率信息 84 | 85 | ### 通用压缩参数 86 | 87 | #### 格式选择 (format) 88 | - **PNG**: 无损压缩,适合需要保持图像质量的场景 89 | - 优点:无损压缩,支持透明度 90 | - 缺点:文件体积相对较大 91 | - 适用场景:图标、截图、需要保持完美质量的图像 92 | - 特点:使用compression_level参数控制压缩强度 93 | 94 | - **WEBP**: Google开发的现代图像格式 95 | - 优点:较好的压缩率,支持有损和无损压缩,支持透明度 96 | - 缺点:兼容性可能不如PNG和JPEG 97 | - 适用场景:网页图像、需要在质量和体积间平衡的场景 98 | - 特点:同时支持quality和alpha_quality参数 99 | 100 | - **JPEG**: 常用的有损压缩格式 101 | - 优点:高压缩率,文件体积小 102 | - 缺点:有损压缩,不支持透明度(透明部分会被转换为白色背景) 103 | - 适用场景:照片、不需要透明度的图像 104 | - 特点:使用quality参数和优化的子采样设置 105 | 106 | #### 质量控制 107 | - **quality**: 压缩质量(1-100,默认:85) 108 | - 对 JPEG 和 WEBP 格式有效 109 | - 值越高,质量越好,文件越大 110 | - 建议值: 111 | - 高质量:85-95 112 | - 平衡质量:75-85 113 | - 高压缩:60-75 114 | 115 | - **compression_level**: PNG压缩级别(0-9,默认:6) 116 | - 只对 PNG 格式有效 117 | - 值越高,压缩率越高,但压缩时间更长 118 | - 建议值: 119 | - 快速压缩:3-4 120 | - 平衡压缩:6 121 | - 最大压缩:9 122 | 123 | - **resize_factor**: 尺寸调整因子(0.1-1.0,默认:1.0) 124 | - 1.0 表示保持原始尺寸 125 | - 小于 1.0 时会按比例缩小图像 126 | - 可用于通过降低分辨率来减小文件大小 127 | 128 | ## 使用建议 129 | 130 | 1. **格式选择**: 131 | - 需要无损压缩:使用 PNG 132 | - 网页图像:优先使用 WEBP 133 | - 照片类图像:使用 JPEG 134 | 135 | 2. **质量设置**: 136 | - PNG:调整 compression_level,建议使用 6-9 137 | - WEBP:quality 设置 80-90 可获得较好的平衡 138 | - JPEG:quality 设置 75-85 通常够用 139 | 140 | 3. **文件大小优化**: 141 | - 首先尝试调整 quality 参数 142 | - 如果仍然太大,可以考虑调整 resize_factor 143 | - 最后考虑更换压缩格式 144 | 145 | ## 注意事项 146 | 147 | - 输出目录会自动创建 148 | - 文件名包含时间戳和计数器,确保不会覆盖已有文件 149 | - 建议根据具体使用场景选择合适的压缩参数 -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # ComfyUI Image Compressor Node 2 | 3 | English | [中文](README.md) 4 | 5 | A ComfyUI custom node for image compression, supporting multiple formats and adjustable parameters. 6 | 7 | ![image](workflow.png) 8 | 9 | > Drag and drop directly into ComfyUI 10 | 11 | ## Features 12 | 13 | - Support multiple image formats (PNG, WEBP, JPEG) 14 | - Adjustable image quality and compression levels 15 | - Support image resizing 16 | - Custom output path and filename prefix 17 | - Display file size before and after compression 18 | - Support batch image processing 19 | - Automatic transparency handling 20 | - Smart handling of various input formats (RGB/RGBA) 21 | 22 | ## Installation 23 | 24 | 1. Copy this plugin to ComfyUI's `custom_nodes` directory 25 | 2. Restart ComfyUI 26 | 3. Find compression nodes in the node list: 27 | - Path: image/processing/Image Compressor (Single image compression node) 28 | - Path: image/processing/Batch Image Compressor (Batch compression node) 29 | 30 | ## Node Types 31 | 32 | ### Image Compressor Node 33 | 34 | #### Input Parameters 35 | - **images**: Input image(s) (supports single or batch images) 36 | - **format**: Output format selection (PNG/WEBP/JPEG) 37 | - **quality**: Compression quality (1-100) 38 | - **resize_factor**: Size adjustment factor (0.1-1.0) 39 | - **compression_level**: Compression level (0-9, PNG only) 40 | - **save_image**: Whether to save the compressed image file (Default: Yes) 41 | - **output_prefix**: Output filename prefix (Default: compressed_) 42 | - **output_path**: Custom output path 43 | - Absolute path: Used directly 44 | - Example: `D:/my_images/compressed` 45 | - Relative path: Based on ComfyUI's output directory 46 | - Example: `my_folder` → `output/my_folder` 47 | - Example: `compressed/png` → `output/compressed/png` 48 | - Empty: Defaults to output/compressed directory 49 | 50 | #### Output Information 51 | - **compression_info**: Compression information string, including: 52 | - Compressed file size 53 | - Original file size 54 | - Save path (if saving is enabled) 55 | - Compression ratio information 56 | - **images**: Compressed images 57 | - Can be directly connected to other nodes requiring image input 58 | - Maintains the original batch structure 59 | - Contains all processed image data after compression 60 | 61 | ### Batch Image Compressor Node 62 | 63 | #### Input Parameters 64 | - **input_path**: Input image directory path 65 | - Processes all image files in the directory 66 | - Automatically recognizes PNG, JPEG, WEBP formats 67 | - Recursively processes images in subdirectories 68 | - **format**: Output format selection (PNG/WEBP/JPEG) 69 | - **quality**: Compression quality (1-100) 70 | - **resize_factor**: Size adjustment factor (0.1-1.0) 71 | - **compression_level**: Compression level (0-9, PNG only) 72 | - **save_image**: Whether to save the compressed image files (Default: Yes) 73 | - **output_prefix**: Output filename prefix (Default: compressed_) 74 | - **output_path**: Custom output path 75 | - Absolute path: Used directly 76 | - Example: `D:/my_images/compressed` 77 | - Relative path: Based on ComfyUI's output directory 78 | - Example: `my_folder` → `output/my_folder` 79 | - Example: `compressed/png` → `output/compressed/png` 80 | - Empty: Defaults to output/compressed directory 81 | 82 | #### Output Information 83 | - **compression_info**: Compression information string, including: 84 | - Compressed file size 85 | - Original file size 86 | - Save path (if saving is enabled) 87 | - Compression ratio information 88 | 89 | ### Common Compression Parameters 90 | 91 | #### Format Selection (format) 92 | - **PNG**: Lossless compression, suitable for scenarios requiring image quality preservation 93 | - Pros: Lossless compression, supports transparency 94 | - Cons: Relatively larger file size 95 | - Use cases: Icons, screenshots, images requiring perfect quality 96 | - Features: Uses compression_level parameter to control compression strength 97 | 98 | - **WEBP**: Modern image format developed by Google 99 | - Pros: Good compression ratio, supports both lossy and lossless compression, transparency support 100 | - Cons: Compatibility might not be as good as PNG and JPEG 101 | - Use cases: Web images, scenarios requiring balance between quality and size 102 | - Features: Supports both quality and alpha_quality parameters 103 | 104 | - **JPEG**: Common lossy compression format 105 | - Pros: High compression ratio, small file size 106 | - Cons: Lossy compression, no transparency support (transparent areas converted to white background) 107 | - Use cases: Photos, images not requiring transparency 108 | - Features: Uses quality parameter and optimized subsampling settings 109 | 110 | #### Quality Control 111 | - **quality**: Compression quality (1-100, Default: 85) 112 | - Effective for JPEG and WEBP formats 113 | - Higher value means better quality but larger file size 114 | - Recommended values: 115 | - High quality: 85-95 116 | - Balanced quality: 75-85 117 | - High compression: 60-75 118 | 119 | - **compression_level**: PNG compression level (0-9, Default: 6) 120 | - Only effective for PNG format 121 | - Higher value means higher compression ratio but longer compression time 122 | - Recommended values: 123 | - Fast compression: 3-4 124 | - Balanced compression: 6 125 | - Maximum compression: 9 126 | 127 | - **resize_factor**: Size adjustment factor (0.1-1.0, Default: 1.0) 128 | - 1.0 means keeping original size 129 | - Values less than 1.0 will proportionally reduce image size 130 | - Can be used to reduce file size by lowering resolution 131 | 132 | ## Usage Recommendations 133 | 134 | 1. **Format Selection**: 135 | - Need lossless compression: Use PNG 136 | - Web images: Prefer WEBP 137 | - Photo-type images: Use JPEG 138 | 139 | 2. **Quality Settings**: 140 | - PNG: Adjust compression_level, recommend 6-9 141 | - WEBP: quality setting 80-90 provides good balance 142 | - JPEG: quality setting 75-85 is usually sufficient 143 | 144 | 3. **File Size Optimization**: 145 | - First try adjusting quality parameter 146 | - If still too large, consider adjusting resize_factor 147 | - Finally consider changing compression format 148 | 149 | ## Notes 150 | 151 | - Output directory is created automatically 152 | - Filenames include timestamp and counter to avoid overwriting existing files 153 | - Recommend selecting appropriate compression parameters based on specific use cases -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .nodes.image_compressor import ImageCompressorNode 2 | from .nodes.batch_image_compressor import BatchImageCompressorNode 3 | 4 | NODE_CLASS_MAPPINGS = { 5 | "ImageCompressor": ImageCompressorNode, 6 | "BatchImageCompressor": BatchImageCompressorNode 7 | } 8 | 9 | NODE_DISPLAY_NAME_MAPPINGS = { 10 | "ImageCompressor": "🐟Image Compressor", 11 | "BatchImageCompressor": "🐟Batch Image Compressor" 12 | } -------------------------------------------------------------------------------- /nodes/base_compressor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | from PIL import Image 4 | import folder_paths 5 | import io 6 | import torch 7 | 8 | class BaseImageCompressor: 9 | """Base class for image compression nodes""" 10 | 11 | def __init__(self): 12 | self.base_output_dir = folder_paths.get_output_directory() 13 | self.output_dir = os.path.join(self.base_output_dir, 'compressed') 14 | self.counter = 0 15 | 16 | def setup_output_path(self, output_path=""): 17 | """Setup output directory""" 18 | if output_path and os.path.isabs(output_path): 19 | self.output_dir = output_path 20 | else: 21 | self.output_dir = os.path.join(self.base_output_dir, output_path.strip('/\\') or 'compressed') 22 | os.makedirs(self.output_dir, exist_ok=True) 23 | 24 | def get_save_options(self, format, quality, compression_level): 25 | """Get save options based on format""" 26 | save_options = {} 27 | if format == "PNG": 28 | save_options.update({'optimize': True, 'compression_level': compression_level}) 29 | elif format == "JPEG": 30 | save_options.update({'quality': quality, 'optimize': True, 'subsampling': 1}) 31 | elif format == "WEBP": 32 | save_options.update({ 33 | 'quality': quality, 34 | 'method': 6, 35 | 'lossless': False, 36 | 'alpha_quality': quality 37 | }) 38 | return save_options 39 | 40 | def process_image(self, img, format, resize_factor, save_options): 41 | """Process single image with resize and format conversion""" 42 | if resize_factor < 1.0: 43 | new_size = tuple(int(dim * resize_factor) for dim in img.size) 44 | img = img.resize(new_size, Image.Resampling.LANCZOS) 45 | 46 | if format == "JPEG" and img.mode == 'RGBA': 47 | background = Image.new('RGB', img.size, (255, 255, 255)) 48 | background.paste(img, mask=img.split()[3]) 49 | img = background 50 | 51 | return img 52 | 53 | def save_image_to_buffer(self, img, format, save_options): 54 | """Save image to buffer and return size info""" 55 | buffer = io.BytesIO() 56 | img.save(buffer, format=format, **save_options) 57 | size = buffer.tell() 58 | size_str = f"{size / (1024 * 1024):.2f}MB" if size >= 1024 * 1024 else f"{size / 1024:.2f}KB" 59 | return buffer, size_str 60 | 61 | def get_original_size(self, img): 62 | """Get original image size in a standardized format""" 63 | buffer = io.BytesIO() 64 | img.save(buffer, format='PNG') 65 | size = buffer.tell() 66 | size_str = f"{size / (1024 * 1024):.2f}MB" if size >= 1024 * 1024 else f"{size / 1024:.2f}KB" 67 | return size_str 68 | 69 | @staticmethod 70 | def get_compression_params(): 71 | """Get common compression parameters""" 72 | params = { 73 | "required": { 74 | "format": (["PNG", "WEBP", "JPEG"],), 75 | "quality": ("INT", { 76 | "default": 85, 77 | "min": 1, 78 | "max": 100, 79 | "step": 1, 80 | "display": "slider" 81 | }), 82 | "resize_factor": ("FLOAT", { 83 | "default": 1.0, 84 | "min": 0.1, 85 | "max": 1.0, 86 | "step": 0.1, 87 | "display": "slider" 88 | }), 89 | "compression_level": ("INT", { 90 | "default": 6, 91 | "min": 0, 92 | "max": 9, 93 | "step": 1, 94 | "display": "slider" 95 | }), 96 | "save_image": ("BOOLEAN", {"default": True}), 97 | "output_prefix": ("STRING", {"default": "compressed_"}), 98 | }, 99 | "optional": { 100 | "output_path": ("STRING", { 101 | "default": "", 102 | "multiline": False 103 | }) 104 | } 105 | } 106 | 107 | return params 108 | 109 | def preprocess_input(self, input_image): 110 | """Preprocess input array to handle special shapes and channel order""" 111 | # Handle special (1, 1, C) shape 112 | if len(input_image.shape) == 3 and input_image.shape[0] == 1 and input_image.shape[1] == 1: 113 | channels = input_image.shape[2] 114 | target_channels = 4 if channels % 4 == 0 else 3 115 | side_length = int(np.sqrt(channels / target_channels)) 116 | if channels % target_channels: 117 | side_length += 1 118 | 119 | flat_data = input_image[0, 0, :channels-(channels % target_channels)].reshape(-1, target_channels) 120 | input_image = np.zeros((side_length, side_length, target_channels), dtype=np.float32) 121 | input_image[:flat_data.shape[0]//side_length, :side_length] = flat_data.reshape(-1, side_length, target_channels) 122 | 123 | # Handle 4D tensor (batch) case 124 | elif len(input_image.shape) == 4: 125 | input_image = input_image[0] 126 | 127 | # Handle channel order 128 | if len(input_image.shape) == 3 and input_image.shape[-1] not in [3, 4]: 129 | if input_image.shape[0] in [3, 4]: 130 | input_image = np.transpose(input_image, (1, 2, 0)) 131 | 132 | # Normalize values to [0, 1] range if needed 133 | if input_image.max() > 1.0: 134 | input_image = input_image / 255.0 135 | 136 | return input_image -------------------------------------------------------------------------------- /nodes/batch_image_compressor.py: -------------------------------------------------------------------------------- 1 | from .base_compressor import BaseImageCompressor 2 | import os 3 | from PIL import Image 4 | import io 5 | 6 | class BatchImageCompressorNode(BaseImageCompressor): 7 | """Batch image compression node""" 8 | 9 | @classmethod 10 | def INPUT_TYPES(cls): 11 | params = cls.get_compression_params() 12 | params["required"]["input_path"] = ("STRING", { 13 | "default": "", 14 | "multiline": False 15 | }) 16 | 17 | return params 18 | 19 | RETURN_TYPES = ("STRING",) 20 | RETURN_NAMES = ("compression_info",) 21 | OUTPUT_NODE = True 22 | FUNCTION = "compress_images" 23 | CATEGORY = "image" 24 | 25 | def compress_images(self, input_path, format, quality=85, resize_factor=1.0, 26 | compression_level=6, save_image=True, output_prefix="compressed_", 27 | output_path=""): 28 | """Compress input images and return compression information""" 29 | self.setup_output_path(output_path) 30 | 31 | # Validate input path 32 | if not os.path.exists(input_path): 33 | return {"result": (f"Input path does not exist: {input_path}",)} 34 | 35 | # Get save options from base class 36 | save_options = self.get_save_options(format, quality, compression_level) 37 | 38 | # Supported image formats 39 | supported_formats = {'.png', '.jpg', '.jpeg', '.webp'} 40 | compressed_sizes = [] 41 | original_sizes = [] 42 | save_paths = [] 43 | 44 | # Process all images in directory 45 | for root, _, files in os.walk(input_path): 46 | for file in files: 47 | if any(file.lower().endswith(fmt) for fmt in supported_formats): 48 | input_file = os.path.join(root, file) 49 | try: 50 | # Read image 51 | img = Image.open(input_file) 52 | 53 | # Get original size info 54 | original_size_str = self.get_original_size(img) 55 | original_sizes.append(original_size_str) 56 | 57 | # Process image using base class method 58 | img = self.process_image(img, format, resize_factor, save_options) 59 | 60 | # Save to buffer and get size info 61 | buffer, size_str = self.save_image_to_buffer(img, format, save_options) 62 | compressed_sizes.append(size_str) 63 | 64 | # Handle file saving 65 | filename = f"{output_prefix}{os.path.splitext(file)[0]}.{format.lower()}" 66 | save_path = os.path.join(self.output_dir, filename) 67 | 68 | if save_image: 69 | with open(save_path, 'wb') as f: 70 | buffer.seek(0) 71 | f.write(buffer.getvalue()) 72 | self.counter += 1 73 | save_path_str = f"{save_path}" 74 | else: 75 | save_path_str = "File not saved" 76 | save_paths.append(save_path_str) 77 | 78 | except Exception as e: 79 | print(f"Failed to process {file}: {str(e)}") 80 | continue 81 | 82 | # For batch processing, we'll join the size and path information with newlines 83 | info_lines = [] 84 | for path, orig, comp in zip(save_paths, original_sizes, compressed_sizes): 85 | info_lines.append(f"{path}: {orig} -> {comp}") 86 | compression_info = "Compression results:\n\n" + "\n".join(info_lines) 87 | 88 | return { 89 | "result": (compression_info,) 90 | } -------------------------------------------------------------------------------- /nodes/image_compressor.py: -------------------------------------------------------------------------------- 1 | from .base_compressor import BaseImageCompressor 2 | import torch 3 | import numpy as np 4 | from PIL import Image 5 | import io 6 | import os 7 | from datetime import datetime 8 | import random 9 | 10 | class ImageCompressorNode(BaseImageCompressor): 11 | """Node for compressing images in ComfyUI""" 12 | 13 | @classmethod 14 | def INPUT_TYPES(cls): 15 | params = cls.get_compression_params() 16 | params["required"]["images"] = ("IMAGE",) 17 | 18 | return params 19 | 20 | RETURN_TYPES = ("STRING", "IMAGE") 21 | RETURN_NAMES = ("compression_info", "images") 22 | OUTPUT_NODE = True 23 | FUNCTION = "compress_image" 24 | CATEGORY = "image" 25 | 26 | def compress_image(self, images, format, quality=85, resize_factor=1.0, 27 | compression_level=6, save_image=True, output_prefix="compressed_", 28 | output_path=""): 29 | """Compress input images and return compression information""" 30 | self.setup_output_path(output_path) 31 | 32 | ui_images = [] 33 | compressed_sizes = [] 34 | original_sizes = [] 35 | save_paths = [] 36 | compressed_images = [] 37 | 38 | # Check if output path is within ComfyUI output directory 39 | try: 40 | base_path = os.path.abspath(self.base_output_dir) 41 | output_path = os.path.abspath(self.output_dir) 42 | is_within_comfyui = output_path.startswith(base_path) 43 | except Exception as e: 44 | is_within_comfyui = False 45 | 46 | # Process each image in the batch 47 | for batch_number, img_tensor in enumerate(images): 48 | # Convert input tensor to numpy array 49 | if not isinstance(img_tensor, torch.Tensor): 50 | img_tensor = torch.from_numpy(img_tensor) 51 | input_image = img_tensor.cpu().numpy() 52 | 53 | # Preprocess input image 54 | input_image = self.preprocess_input(input_image) 55 | 56 | # Convert to PIL Image with proper alpha channel handling 57 | if input_image.shape[-1] == 4: 58 | # RGBA image 59 | img = Image.fromarray((input_image * 255).astype(np.uint8), 'RGBA') 60 | else: 61 | # RGB image 62 | img = Image.fromarray((input_image * 255).astype(np.uint8), 'RGB') 63 | 64 | # Get original size info 65 | original_size_str = self.get_original_size(img) 66 | original_sizes.append(original_size_str) 67 | 68 | # Get save options from base class 69 | save_options = self.get_save_options(format, quality, compression_level) 70 | 71 | # Process image using base class method 72 | img = self.process_image(img, format, resize_factor, save_options) 73 | 74 | # Save to buffer and get size info 75 | buffer, size_str = self.save_image_to_buffer(img, format, save_options) 76 | compressed_sizes.append(size_str) 77 | 78 | # Handle file saving and UI info 79 | timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f') # Add milliseconds 80 | random_suffix = random.randint(1000, 9999) # 4-digit random number 81 | filename = f"{output_prefix}{timestamp}_{self.counter:04d}_{random_suffix}.{format.lower()}" 82 | save_path = os.path.join(self.output_dir, filename) 83 | 84 | if save_image: 85 | with open(save_path, 'wb') as f: 86 | buffer.seek(0) 87 | f.write(buffer.getvalue()) 88 | self.counter += 1 89 | save_path_str = f"{save_path}" 90 | # Only add to UI images if within ComfyUI output directory 91 | if is_within_comfyui: 92 | ui_images.append({ 93 | "filename": filename, 94 | "subfolder": self.output_dir, 95 | "type": 'output' 96 | }) 97 | else: 98 | save_path_str = "File not saved" 99 | save_paths.append(save_path_str) 100 | 101 | # For batch processing, we'll join the size and path information with newlines 102 | info_lines = [] 103 | for path, orig, comp in zip(save_paths, original_sizes, compressed_sizes): 104 | info_lines.append(f"{path}: {orig} -> {comp}") 105 | compression_info = "Compression results:\n\n" + "\n".join(info_lines) 106 | 107 | # Load the compressed image from buffer 108 | buffer.seek(0) 109 | compressed_img = Image.open(buffer) 110 | compressed_img.load() # Make sure the image is fully loaded 111 | 112 | # Convert compressed image to tensor for output 113 | if compressed_img.mode == 'RGBA': 114 | img_np = np.array(compressed_img).astype(np.float32) / 255.0 115 | else: 116 | # 如果原图有 alpha 通道但压缩后没有(比如 JPEG),使用 RGB 模式 117 | if len(img_tensor.shape) > 2 and img_tensor.shape[-1] == 4: 118 | rgb_img = compressed_img.convert('RGB') 119 | img_np = np.array(rgb_img).astype(np.float32) / 255.0 120 | else: 121 | img_np = np.array(compressed_img.convert('RGB')).astype(np.float32) / 255.0 122 | if len(img_np.shape) == 2: 123 | img_np = np.stack([img_np] * 3, axis=-1) 124 | 125 | if len(img_tensor.shape) == 4: 126 | img_np = np.expand_dims(img_np, 0) 127 | 128 | # Convert numpy array to torch tensor for ComfyUI compatibility 129 | img_to_tensor = torch.from_numpy(img_np).to(img_tensor.device) 130 | # Add processed image to list 131 | compressed_images.append(img_to_tensor) 132 | 133 | # Only include UI images if within ComfyUI output directory 134 | result = {"result": (compression_info, compressed_images)} 135 | if is_within_comfyui and ui_images: 136 | result["ui"] = {"images": ui_images} 137 | return result 138 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "comfyui-image-compressor" 3 | description = "A ComfyUI custom node for image compression that supports multiple compression formats and parameter adjustments." 4 | version = "1.0.0" 5 | license = {file = "LICENSE"} 6 | dependencies = ["Pillow>=10.0.0", "numpy>=1.24.0", "torch>=2.0.0"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/liuqianhonga/ComfyUI-Image-Compressor" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "liuqianhong" 14 | DisplayName = "ComfyUI-Image-Compressor" 15 | Icon = "🐟" 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow>=10.0.0 2 | numpy>=1.24.0 3 | torch>=2.0.0 -------------------------------------------------------------------------------- /workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuqianhonga/ComfyUI-Image-Compressor/cc458d7f01355d698c542f9321d4651aef34fa08/workflow.png --------------------------------------------------------------------------------