44 Commits
Author SHA1 Message Date
Salvador E. Tropea 1078a8a9e0 Bumped version to 1.4.0 2025-10-22 13:23:05 -03:00
Salvador E. Tropea e939a863c2 [Added] "Image with text label" node 2025-10-20 10:50:03 -03:00
Salvador E. Tropea 1d1b99ac49 [Added] Arbitrary normalization and a node to input the parameters 2025-10-14 11:40:16 -03:00
Salvador E. Tropea c5060bafe6 [Added] Icon for the ComfyUI registry 2025-10-14 09:31:35 -03:00
Salvador E. Tropea 055236953d [Added] Icon image 2025-10-14 09:30:54 -03:00
Salvador E. Tropea 346fc8590b [DOCs] Updated the nodes description 2025-10-14 09:16:53 -03:00
Salvador E. Tropea 2c732ce7cb Bumped version to 1.3.0 2025-10-14 09:11:11 -03:00
Salvador E. Tropea e7d124d786 [ImagePad][Added] Copy input size when target_* is 0 2025-10-14 09:04:32 -03:00
Salvador E. Tropea c976f3d057 [DOCs] And another way to display the examples 2025-10-14 08:15:32 -03:00
Salvador E. Tropea c74e859aaa [DOCs] Trying a different way to show the examples 2025-10-14 08:14:28 -03:00
Salvador E. Tropea 684d3fcb6b [DOCs][Added] Image for the examples 2025-10-14 08:12:22 -03:00
Salvador E. Tropea 0f0157ed4f [DOCs] Moved node docs to a separated file
To make the README.md more clear
2025-10-14 08:07:31 -03:00
Salvador E. Tropea b306bd37a7 [DOCs] Separated nodes in categories (in the ToC) 2025-10-14 07:57:47 -03:00
Salvador E. Tropea ea94753216 [DOCs] Made the example links to point to raw files, easier to download 2025-10-14 07:52:54 -03:00
Salvador E. Tropea 3844dd20b5 [DOCs][Added] Documentation about the new nodes 2025-10-14 07:48:32 -03:00
Salvador E. Tropea 79f1003873 [Added] Resize example 2025-10-13 20:11:08 -03:00
Salvador E. Tropea e159265648 [ImagePad/Resize][Added] Control over padding transparency
By default is transparent
2025-10-13 20:00:58 -03:00
Salvador E. Tropea aa8dc7b2ce [Download][Added] Option to create RGBA images 2025-10-13 19:17:13 -03:00
Salvador E. Tropea 684afdf5e6 [ImageResize][Fixed] Mask processing for modes others than edge_pixel
Avoid replicating the edge for other modes.
2025-10-13 18:24:30 -03:00
Salvador E. Tropea 314c6bf609 [ResizeImage][Fixed] Comment about the difference when w/h==0 2025-10-13 17:50:04 -03:00
Salvador E. Tropea 529756d415 [ImagePad][Fixed] Error when padding an RGBA image 2025-10-13 17:47:54 -03:00
Salvador E. Tropea 79b14305d5 [ImageResize][Fixed] Size computation when w/h are 0 2025-10-13 13:24:30 -03:00
Salvador E. Tropea 968859fc5a [ImageResize][Fixed] Color must be [0,1] 2025-10-13 13:23:23 -03:00
Salvador E. Tropea f05c36f520 [Nodes][Added] ResizeMask also synced with Kijai 1.1.7 2025-10-06 13:36:24 -03:00
Salvador E. Tropea d99c165f3d Merge branch 'resize_pad'
Some operations are needed and Comfy-Core lacks them
2025-10-05 14:42:14 -03:00
Salvador E. Tropea 8134be8032 Bumped version to 1.2.0 2025-10-05 11:24:35 -03:00
Salvador E. Tropea 8d587bcab4 [Fixed][Downloader] Display image with the upstream name
Not the one we downloaded
2025-10-04 18:31:08 -03:00
Salvador E. Tropea 2ddd61ee87 [Added] Foreground estimation (2 nodes) and Empty Image (from ref) 2025-09-30 10:05:32 -03:00
Salvador E. Tropea 87ee4aebb7 [Apply Mask][Added] Option to process images one by one 2025-09-17 13:37:25 -03:00
Salvador E. Tropea d95ef9758d [Added][Downloader] Name for the local image 2025-09-17 09:20:21 -03:00
Salvador E. Tropea 01945ade86 [Added] Node to apply a mask to an image
Using Approximate Fast Foreground Colour Estimation
2025-09-16 13:50:05 -03:00
Salvador E. Tropea 015f8be8c9 [Added] Resize and Pad image from KJNodes
Just to keep dependencies low
But current Comfy-Core can handle it
2025-09-16 08:36:55 -03:00
Salvador E. Tropea 644a2fa7f4 [Added] Nodes for normalization
Useful for testing models or using low level models
2025-09-16 08:35:49 -03:00
Salvador E. Tropea 0d3c321ae3 [Added][Tools] Style fixer 2025-08-22 11:17:31 -03:00
Salvador E. Tropea 9a565f31b0 [Added] Ignore the sync script 2025-08-22 11:16:57 -03:00
Salvador E. Tropea 214bee12d9 [Added] Nodes to insert a video in a reference/s 2025-08-22 11:16:03 -03:00
Salvador E. Tropea a091559e25 [Fixed] Flake8 style details 2025-08-22 09:59:45 -03:00
Salvador E. Tropea bb890e2027 Bumped version to 1.1.1 2025-07-25 13:12:53 -03:00
Salvador E. Tropea d989087f08 Added (deprecated) requirements.txt
comfy-cli is really wrong
2025-07-25 13:11:58 -03:00
Salvador E. Tropea 13be95db99 [README][Added] SeCoNoHe as dependency 2025-07-25 12:58:58 -03:00
Salvador E. Tropea 018f62fed0 [Pre Commit][Added] Version check
To overcome comfy-cli limitations
2025-07-25 12:49:49 -03:00
Salvador E. Tropea d443749537 Adapted the project description to the obsolete format
comfy-cli enforces deprecated practices.
2025-07-25 12:48:37 -03:00
Salvador E. Tropea 35f1f1b772 [Nodes] Removed unneeded exclusion 2025-07-25 12:48:05 -03:00
Salvador E. Tropea 624e311edf [Examples] Removed extra new line 2025-07-25 12:47:28 -03:00
14 changed files with 2132 additions and 38 deletions
+1
View File
@@ -196,3 +196,4 @@ cython_debug/
*.dst
*.epr
.*~
sync.sh
+16 -1
View File
@@ -51,7 +51,7 @@ repos:
# The command to execute. It's expected to be found in the PATH
# of the environment where you run `git commit`.
entry: mypy
args: ["--explicit-package-bases", "source/"]
args: ["--explicit-package-bases", "src/"]
# 'system' tells pre-commit to find the command in the current environment
# instead of building an isolated one.
language: system
@@ -64,3 +64,18 @@ repos:
# whole project as configured in mypy.ini, rather than just the changed files.
# Try without it first.
# pass_filenames: false
# --- Version checking ---
- repo: local
hooks:
- id: version-check
name: check for version consistency
# The command to execute. It's a Python script.
entry: python tools/check_versions.py
# Use 'system' to run it with the current environment's Python
language: system
# This hook doesn't need to run on every file.
# It should run if either of the version files change.
# This makes it very fast.
files: ^(pyproject\.toml|src/nodes/__init__\.py)$
# The regex `^...$` ensures it matches the full path from the repo root.
+54 -24
View File
@@ -1,13 +1,12 @@
# ComfyUI Image Miscellaneous Nodes 🎨
This repository provides a set of custom nodes for ComfyUI focused on image manipulation.
Currently we just have an image downloader node, but I didn't wanted to put it with
my audio nodes.
Currently we just have a few nodes used by other nodes I maintain.
## ⚙️ Main features
✅ No extra dependencies, we use the same modules as ComfyUI
✅ No bizarre extra dependencies, we use the same modules as ComfyUI
✅ Warnings and errors visible in the browser, configurable debug information in the console
@@ -18,7 +17,27 @@ my audio nodes.
- 📦 [Dependencies](#-dependencies)
- 🖼️ [Examples](#️-examples)
- ✨ [Nodes](#-extra-nodes)
- [1. Image Download and Load](#1-image-download-and-load)
- Foreground: (Foreground estimation)
- [Estimate foreground (AFFCE)](docs/nodes_img.md#8-estimate-foreground-affce)
- [Estimate foreground (FMLFE)](docs/nodes_img.md#9-estimate-foreground-fmlfe)
- Generation:
- [Create Empty Image](docs/nodes_img.md#10-create-empty-image)
- Input/Output:
- [Image Download and Load](docs/nodes_img.md#1-image-download-and-load)
- Manipulation:
- [Face Composite](docs/nodes_img.md#2-face-composite)
- [Face Composite (frame by frame)](docs/nodes_img.md#3-face-composite-frame-by-frame)
- [Apply Mask using AFFCE](docs/nodes_img.md#7-apply-mask-using-affce)
- [Pad Image](docs/nodes_img.md#11-pad-image-kjset)
- [Resize Image](docs/nodes_img.md#12-resize-image-kjset)
- [Resize Mask](docs/nodes_img.md#13-resize-mask-kjset)
- [Image with text label](docs/nodes_img.md#16-image-with-text-label)
- Normalization:
- [Normalize Image to ImageNet](docs/nodes_img.md#4-normalize-image-to-imagenet)
- [Normalize Image to [-0.5, 0.5]](docs/nodes_img.md#5-normalize-image-to-05-05)
- [Normalize Image to [-1, 1]](docs/nodes_img.md#6-normalize-image-to-1-1)
- [Arbitrary Normalize](docs/nodes_img.md#14-arbitrary-normalize)
- [Normalize Parameters](docs/nodes_img.md#15-normalize-parameters)
- 📝 [Usage Notes](#-usage-notes)
- 📜 [Project History](#-project-history)
- ⚖️ [License](#️-license)
@@ -26,23 +45,7 @@ my audio nodes.
## ✨ Nodes
### 1. Image Download and Load
- **Display Name:** `Image Download and Load`
- **Internal Name:** `SET_ImageDownload`
- **Category:** `image/io`
- **Description:** Downloads an image file from a URL into the `ComfyUI/input/` directory if it's not already there, and then loads it as an image and mask. This is perfect for creating self-contained, shareable workflows with example image.
- **Inputs:**
- `image_bypass` (IMAGE, Optional): If an image is provided here it will be used for the output. You can connect a `Load Image` node here, if the connected node is muted (bypassed) we download the file, otherwise we use the image from the `Load Image` node.
- `mask_bypass` (MASK, Optional): This input complements `image_bypass`.
- `base_url` (STRING): The URL of the directory containing the image file.
- `filename` (STRING): The name of the image file to download (e.g., photo.jpg, art.png).
- **Output:**
- `image` (IMAGE): The loaded image.
- `alpha_mask` (MASK): The alpha mask for the loaded image.
- **Behavior Details:**
- **Caching:** The node checks the `ComfyUI/input/` folder first. If the file with the specified `filename` already exists, the download is skipped.
- **Bypass:** If only one of `image_bypass` and `mask_bypass` is connected the other will be assumed to be empty. You should connect both or avoid using the output corresponding to the unconnected input.
The nodes are documented [here](docs/nodes_img.md). Use the above ToC to access them by category.
## 🚀 Installation
@@ -53,14 +56,18 @@ You can install the nodes from the ComfyUI nodes manager, the name is *Image Mis
cd ComfyUI/custom_nodes/
git clone https://github.com/set-soft/ComfyUI-ImageMisc ComfyUI-ImageMisc
```
2. Restart ComfyUI.
2. Install dependencies: `pip install -r ComfyUI/custom_nodes/ComfyUI-ImageMisc/requirements.txt`
3. Restart ComfyUI.
The nodes should then appear under the "image/io" category in the "Add Node" menu.
The nodes should then appear under the "image/io", "image/manipulation", "image/normalization" and "image/foreground" categories in the "Add Node" menu.
## 📦 Dependencies
- SeCoNoHe (seconohe): This is just some functionality I wrote shared by my nodes, only depends on ComfyUI.
- PyTorch: Installed by ComfyUI
- NumPy: Installed by ComfyUI
- Pillow: Installed by ComfyUI
- Requests (optional): Usually an indirect ComfyUI dependency. If installed it will be used for downloads, it should be more robust than then built-in `urllib`, used as fallback.
- Colorama (optional): Might help to get colored log messages on some terminals. We use ANSI escape sequences when it isn't installed.
@@ -69,7 +76,18 @@ The nodes should then appear under the "image/io" category in the "Add Node" men
Once installed the examples are available in the ComfyUI workflow templates, in the *Image Misc* section (or ComfyUI-ImageMisc).
- [image_download.json](example_workflows/image_download.json): Shows how to use the image downloader node.
### [image_download.json](https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/refs/heads/main/example_workflows/image_download.json)
Shows how to use the image downloader node.
[![image_download.json](example_workflows/image_download.jpg)](https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/refs/heads/main/example_workflows/image_download.json)
### [resize.json](https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/refs/heads/main/example_workflows/resize.json)
Shows the most common uses of the resize and padding nodes. You can see how various modes and options affects the result and how the different interpolation methods works. It shows how masks and RGBA images are handled.
[![resize.json](example_workflows/resize.jpg)](https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/refs/heads/main/example_workflows/resize.json)
## 📝 Usage Notes
@@ -84,6 +102,17 @@ Once installed the examples are available in the ComfyUI workflow templates, in
- 1.0.0 2025-07-21: Initial release
- Just the download image.
- 1.1.0 2025-07-25: Adapted to use SeCoNoHe
- 1.1.1 2025-07-25: Just small details for ComfyUI registry
- 1.2.0 2025-10-05:
- Face composite nodes
- Normalization nodes
- Apply mask nodes, including support for:
- "Fast Multi-Level Foreground Estimation" (from [PyMatting](https://github.com/pymatting/pymatting))
- "Approximate Fast Foreground Colour Estimation" (from [Photoroom](https://github.com/Photoroom/fast-foreground-estimation))
- Create empty image node, from reference image
- 1.3.0 2025-10-14: Pad Image, Resize Image and Resize Mask nodes
- 1.4.0 2025-10-20: "Image with text label" and "Arbitrary Normalize"/"Normalize Parameters"
## ⚖️ License
@@ -93,3 +122,4 @@ Once installed the examples are available in the ComfyUI workflow templates, in
## 🙏 Attributions
- Good part of the initial code and this README was generated using Gemini 2.5 Pro.
- The *Pad Image*, *Resize Image* and *Resize Mask* are from [KJNodes](https://github.com/kijai/ComfyUI-KJNodes) by [Kijai](https://github.com/kijai) (Jukka Seppänen)
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+307
View File
@@ -0,0 +1,307 @@
# 1. Image Download and Load
- **Display Name:** `Image Download and Load`
- **Internal Name:** `SET_ImageDownload`
- **Category:** `image/io`
- **Description:** Downloads an image file from a URL into the `ComfyUI/input/` directory if it's not already there, and then loads it as an image and mask. This is perfect for creating self-contained, shareable workflows with example image.
- **Inputs:**
- `image_bypass` (IMAGE, Optional): If an image is provided here it will be used for the output. You can connect a `Load Image` node here, if the connected node is muted (bypassed) we download the file, otherwise we use the image from the `Load Image` node.
- `mask_bypass` (MASK, Optional): This input complements `image_bypass`.
- `base_url` (STRING): The URL of the directory containing the image file.
- `filename` (STRING): The name of the image file to download (e.g., photo.jpg, art.png).
- `local_name` (STRING, optional): The name used locally. Leave blank to use `filename`.
- `embed_transparency` (BOOLEAN, optional): Create RGBA images when they have transparency. Not all nodes supports them.
- **Output:**
- `image` (IMAGE): The loaded image.
- `alpha_mask` (MASK): The alpha mask for the loaded image.
- **Behavior Details:**
- **Caching:** The node checks the `ComfyUI/input/` folder first. If the file with the specified `filename` already exists, the download is skipped.
- **Bypass:** If only one of `image_bypass` and `mask_bypass` is connected the other will be assumed to be empty. You should connect both or avoid using the output corresponding to the unconnected input.
# 2. Face Composite
- **Display Name:** `Face Composite`
- **Internal Name:** `SET_CompositeFace`
- **Category:** `image/manipulation`
- **Description:** This node is designed for complex batch operations where a single reference image generates multiple animated or processed frames. It handles an **M-to-N** relationship.
- **Purpose:** The primary use case is for animation workflows (e.g., using AnimateDiff or SVD) where you:
1. Extract a face from **one** source image.
2. Generate **N** animated frames of that face.
3. Paste each of the **N** animated frames back onto the original source image to create an animated sequence.
- **Inputs:**
- `animated` (`IMAGE`): A batch of `M*N` cropped/processed images (e.g., the animated faces).
- `reference` (`IMAGE`): A batch of `M` original context images that the faces were extracted from.
- `bboxes` (`BBOX`): A list of `M` bounding boxes. Each box in the list must correspond to a `reference` image, defining the location for the paste operation. Format: (X, Y, W, H)
- **Output:**
- `images` (`IMAGE`): The final output batch of `M*N` full-size images. Each image consists of a `reference` image with one of the `animated` frames pasted onto it.
- **How it Works:**
- For each of the `M` reference images and its corresponding bounding box, the node takes the next `N` frames from the `animated` batch. It then pastes each of these `N` frames onto a *copy* of the reference image, generating `N` final output frames. This process is repeated for all `M` reference images.
- The animated frame is automatically resized to fit the bounding box dimensions before pasting. The pasting logic safely handles cases where the bbox is partially outside the image boundaries.
# 3. Face Composite (frame by frame)
- **Display Name:** `Face Composite (frame by frame)`
- **Internal Name:** `SET_CompositeFaceFrameByFrame`
- **Category:** `image/manipulation`
- **Description:** This node is a simplified variant for **1-to-1** compositing. It is perfect for video processing workflows where you need to paste a sequence of processed frames back into the original video sequence at a static location.
- **Purpose:** Ideal for tasks like video face restoration, stylization, or simple lip-sync, where:
1. A video is loaded as a sequence of `N` frames.
2. A corresponding sequence of `N` processed/animated frames is generated.
3. Each processed frame needs to be pasted back into its corresponding original frame at the same location.
- **Inputs:**
- `animated` (`IMAGE`): A batch of `N` processed frames.
- `reference` (`IMAGE`): A batch of `N` original frames. **Must have the same batch size as `animated`**.
- `bboxes` (`BBOX`): A list of bounding boxes. **Only the first bbox in the list is used** as the static paste location for all frames.
- **Output:**
- `images` (`IMAGE`): The final batch of `N` composited frames.
- **How it Works:** The node iterates from frame 0 to N-1. In each step, it takes the i-th `animated` frame and the i-th `reference` frame. It then pastes the animated frame onto the reference frame using the coordinates from the single, static bounding box.
# 4. Normalize Image to ImageNet
- **Display Name:** `Normalize Image to ImageNet`
- **Internal Name:** `SET_NormalizeToImageNetDataset`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor using the mean and standard deviation of the ImageNet dataset.
- **Purpose:** Essential for pre-processing images before feeding them into models that were pre-trained on ImageNet (e.g., most ResNet, VGG, EfficientNet models).
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor. The value range will be altered significantly.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using the standard ImageNet values:
- **Mean:** `[0.485, 0.456, 0.406]`
- **Std Dev:** `[0.229, 0.224, 0.225]`
# 5. Normalize Image to [-0.5, 0.5]
- **Display Name:** `Normalize Image to [-0.5, 0.5]`
- **Internal Name:** `SET_NormalizeToMinus05_05`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor by centering its values around zero.
- **Purpose:** Useful for models trained from scratch or those that expect input data in the `[-0.5, 0.5]` range. This can help stabilize training.
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor, with values in the `[-0.5, 0.5]` range.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using:
- **Mean:** `[0.5, 0.5, 0.5]`
- **Std Dev:** `[1.0, 1.0, 1.0]`
# 6. Normalize Image to [-1, 1]
- **Display Name:** `Normalize Image to [-1, 1]`
- **Internal Name:** `SET_NormalizeToMinus1_1`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor to the `[-1, 1]` range.
- **Purpose:** A common requirement for certain model architectures, particularly Generative Adversarial Networks (GANs) and models using the `tanh` activation function in their output layer.
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor, with values in the `[-1, 1]` range.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using:
- **Mean:** `[0.5, 0.5, 0.5]`
- **Std Dev:** `[0.5, 0.5, 0.5]`
# 7. Apply Mask using AFFCE
- **Display Name:** `Apply Mask using AFFCE`
- **Internal Name:** `SET_ApplyMaskAFFCE`
- **Category:** `image/manipulation`
- **Description:** Applies a mask to an image using [Approximate Fast Foreground Colour Estimation](https://github.com/Photoroom/fast-foreground-estimation). This blends the image contour in a better way.
- **Purpose:** Used to apply the mask of a background removal model.
- **Inputs:**
- `images` (`IMAGE`): One ore more ComfyUI images
- `masks` (`MASK`): Masks to apply
- `blur_size` (`INT`): Diameter for the coarse gaussian blur
- `blur_size_two` (`INT`): Diameter for the fine gaussian blur
- `fill_color` (`BOOLEAN`): When enabled the removed image is replaced by a color, the output is an RGB image. Otherwise the removed part becomes transparent and the output is an RGBA image.
- `color` (`STRING`): A string representing a color to be used when `fill_color` is enabled. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range.
- `batched` (`BOOLEAN`): Process all the images at once. Otherwise do it one at a time.
- **Output:**
- `image` (`IMAGE`): The image after applying the mask.
- `mask` (`MASK`): The input mask
# 8. Estimate foreground (AFFCE)
- **Display Name:** `Estimate foreground (AFFCE)`
- **Internal Name:** `SET_AFFCE`
- **Category:** `image/foreground`
- **Description:** Estimates the foreground of an image using [Approximate Fast Foreground Colour Estimation](https://github.com/Photoroom/fast-foreground-estimation). The result is suitable for background replacement.
- **Purpose:** Used to get a better foreground for background removal.
- **Inputs:**
- `images` (`IMAGE`): One ore more ComfyUI images
- `masks` (`MASK`): Masks to apply
- `blur_size` (`INT`): Diameter for the coarse gaussian blur
- `blur_size_two` (`INT`): Diameter for the fine gaussian blur
- `batched` (`BOOLEAN`): Process all the images at once. Otherwise do it one at a time.
- **Output:**
- `image` (`IMAGE`): The image after foreground estimation.
- `mask` (`MASK`): The input mask
# 9. Estimate foreground (FMLFE)
- **Display Name:** `Estimate foreground (FMLFE)`
- **Internal Name:** `SET_FMLFE`
- **Category:** `image/foreground`
- **Description:** Estimates the foreground of an image using [Fast Multi-Level Foreground Estimation](https://arxiv.org/abs/2006.14970). The result is suitable for background replacement.
- **Purpose:** Used to get a better foreground for background removal.
- **Inputs:**
- `images` (`IMAGE`): The source image(s) from which to estimate the foreground and background.
- `masks` (`MASK`): The alpha matte that guides the estimation. White areas are treated as known foreground, black as known background, and gray areas are the semi-transparent regions the algorithm will solve for.
- `implementation" (`auto`, `cupy`, `opencl`, `numba`, `torch`): Which implementation to use. The `auto` will use the fastest available. The `torch` implementation is slow and approximated, but doesn't need extra dependencies. The `numba` implementation is good and just needs [Numba](https://numba.pydata.org/). The `cupy` implementation is the fastest, but needs [CuPy](https://cupy.dev/) and a full CUDA environment.
- `regularization` (`FLOAT`): The regularization strength (epsilon). This acts as a smoothness prior. Higher values result in smoother, more blended foreground and background colors, but may lose very fine details. Lower values preserve more detail but can be noisier.
- `n_small_iterations` (`INT`): The number of solver iterations to perform on the lower-resolution levels of the image pyramid. More iterations can improve quality at the cost of speed.
- `n_big_iterations` (`INT`): The number of solver iterations to perform on the higher-resolution (larger) levels of the image pyramid. Fewer iterations are typically needed at high resolution as the details are propagated up from the smaller levels.
- `small_size` (`INT`): The pixel dimension threshold. Image pyramid levels smaller than this size will use the higher 'n_small_iterations' count, while larger levels will use 'n_big_iterations'.
- `gradient_weight` (`FLOAT`): Controls how strongly the edges in the alpha matte influence color blending. A higher value makes the algorithm respect the mask's edges more, leading to sharper color boundaries. A lower value allows more color bleeding, an effect similar to increasing regularization.
- **Output:**
- `image` (`IMAGE`): The estimated foreground image (F). This is a full-color image where the algorithm has estimated the true, un-blended color of the foreground object.
- `mask` (`MASK`): The estimated background image (B). The algorithm has effectively "inpainted" the area behind the foreground object, creating a clean background plate.
# 10. Create Empty Image
- **Display Name:** `Create Empty Image`
- **Internal Name:** `SET_CreateEmptyImage`
- **Category:** `image/generation`
- **Description:** Creates an image filled with a solid color. Similar to standard `EmptyImage`, but you can use an image as reference and you have more options to select the color.
- **Purpose:** Create an empty image
- **Inputs:**
- `width` (`INT`): The width of the new image in pixels. This value is ignored if a `reference` is provided.
- `height` (`INT`): The height of the new image in pixels. This value is ignored if a `reference` is provided.
- `batch_size` (`INT`): The number of images to create in the batch. This value is ignored if a `reference` is provided.
- `color` (`STRING`): The solid color to fill the image with. Can be a named color (e.g., "black", "red"), a hex string (e.g., "#FF0000", "00ff00"), or comma-separated components in the [0, 255] or [0.0, 1.0] range.
- `reference` (`IMAGE`, optional): If an image is connected here, its dimensions (batch size, height, and width) will be used for the new image, overriding the manual width, height, and batch_size inputs.
- **Output:**
- `image` (`IMAGE`): A new image tensor of the specified size and color.
# 11. Pad Image (KJ/SET)
- **Display Name:** `Pad Image (KJ/SET)`
- **Internal Name:** `SET_ImagePad`
- **Category:** `image/manipulation`
- **Description:** Pads the input image and an optional mask with specified padding on each side. It offers several modes to fill the padded area.
- **Purpose:** To add borders or expand the canvas of an image using various fill methods, such as solid color, edge pixel replication, or a blurred background effect.
- **Inputs:**
- `image` (`IMAGE`): The image to be padded.
- `left` (`INT`): The number of pixels of padding to add to the left side.
- `right` (`INT`): The number of pixels of padding to add to the right side.
- `top` (`INT`): The number of pixels of padding to add to the top side.
- `bottom` (`INT`): The number of pixels of padding to add to the bottom side.
- `extra_padding` (`INT`): Additional padding applied to all sides. Note: If `target_width` and `target_height` are used, this value will shrink the source image before padding.
- `pad_mode` (`STRING`): The method used to fill the padded area.
- `edge`: Fills with the average color of the nearest edge.
- `edge_pixel`: Repeats the outermost line of pixels.
- `color`: Fills with the specified solid `color`.
- `pillarbox_blur`: Fills with a blurred, desaturated, and dimmed version of the original image, scaled to fit the new dimensions.
- `color` (`STRING`): The solid color to use when `pad_mode` is "color". Accepts hex codes, color names, and RGB values. This is more flexible than in KJNodes.
- `mask` (`MASK`, optional): An optional mask to be padded along with the image.
- `target_width` (`INT`, optional): If provided with `target_height`, pads the image to this exact width, centering the original content. This overrides the `left` and `right` inputs. If you set it to 0 the width of the image is used. Note that you must also connect `target_height`.
- `target_height` (`INT`, optional): If provided with `target_width`, pads the image to this exact height, centering the original content. This overrides the `top` and `bottom` inputs. If you set it to 0 the heigth of the image is used. Note that you must also connect `target_width`.
- `pad_transparency` (`FLOAT`, optional): Controls the opacity of the padded area in the output mask. 1.0 is fully transparent (no mask), and 0.0 is fully opaque (masked). Not in KJNodes.
- **Outputs:**
- `images` (`IMAGE`): The padded image tensor.
- `masks` (`MASK`): The padded mask tensor.
# 12. Resize Image (KJ/SET)
- **Display Name:** `Resize Image (KJ/SET)`
- **Internal Name:** `SET_ImageResize`
- **Category:** `image/manipulation`
- **Description:** A comprehensive node to resize, crop, and pad images. It provides multiple methods for handling aspect ratio and can optionally process large batches in smaller chunks to conserve memory. This node uses [Pad Image](#11-pad-image-kjset) so please also read its documentation.
- **Purpose:** To change the dimensions of an image with fine-grained control over how the aspect ratio is preserved, including stretching, cropping, or padding to fit the target size.
- **Inputs:**
- `image` (`IMAGE`): The source image to resize.
- `width` (`INT`): The target width in pixels. If set to 0, it uses the original width or the width from `get_image_size`.
- `height` (`INT`): The target height in pixels. If set to 0, it uses the original height or the height from `get_image_size`.
- `upscale_method` (`STRING`): The interpolation algorithm to use for resizing (e.g., `bicubic`, `lanczos`, `nearest-exact`).
- `keep_proportion` (`STRING`): Defines how to handle the aspect ratio when resizing.
- `stretch`: Ignores aspect ratio and forces the image to the exact target dimensions.
- `resize`: Maintains aspect ratio, fitting the image within the target dimensions. The final output size may be smaller than the target on one axis.
- `pad`, `pad_edge`, `pad_edge_pixel`: Resizes the image to fit within the target dimensions while maintaining aspect ratio, then pads the remaining space to meet the exact target dimensions.
- `crop`: Resizes and crops the image to match the target aspect ratio and size.
- `pillarbox_blur`: Resizes the image to fit and fills the remaining space with a blurred background.
- `pad_color` (`STRING`): The solid color to use for padding when `keep_proportion` is set to `pad`. Accepts hex codes, color names, and RGB values. This is more flexible than in KJNodes.
- `crop_position` (`STRING`): The anchor point (`center`, `top`, `left`, etc.) for `crop` and `pad` operations.
- `divisible_by` (`INT`): Ensures the final width and height are multiples of this number, which is often required for models like VAEs.
- `mask` (`MASK`, optional): An optional mask to be transformed along with the image.
- `device` (`STRING`, optional): The computation device (`cpu` or `gpu`) to use for the operation. Note that `lanczos` is not supported on the GPU.
- `get_image_size` (`IMAGE`, optional): If an image is connected, its dimensions will be used as the target width and height, overriding the manual inputs. Not in KJNodes.
- `per_batch` (`INT`, optional): If greater than 0, processes the input batch in smaller sub-batches of this size to reduce memory usage.
- `pad_transparency` (`FLOAT`, optional): Controls the opacity of the padded area in the output mask. Not in KJNodes.
- **Outputs:**
- `IMAGE` (`IMAGE`): The transformed image.
- `width` (`INT`): The final width of the output image.
- `height` (`INT`): The final height of the output image.
- `mask` (`MASK`): The transformed mask.
# 13. Resize Mask (KJ/SET)
- **Display Name:** `Resize Mask (KJ/SET)`
- **Internal Name:** `SET_ResizeMask`
- **Category:** `image/manipulation`
- **Description:** Resizes a mask or a batch of masks to the specified width and height, with options to preserve proportions.
- **Purpose:** A dedicated and simplified node for resizing masks, which often require different interpolation methods (like `nearest-exact`) than images.
- **Inputs:**
- `mask` (`MASK`): The mask to be resized.
- `width` (`INT`): The target width for the new mask.
- `height` (`INT`): The target height for the new mask.
- `keep_proportions` (`BOOLEAN`): If `True`, maintains the original aspect ratio, fitting the mask within the target dimensions.
- `upscale_method` (`STRING`): The interpolation algorithm to use for resizing. Defaults to `nearest-exact`, which is ideal for masks to avoid creating grayscale values.
- `crop` (`STRING`): If and how to crop the mask (`disabled` or `center`).
- `get_image_size` (`IMAGE`, optional): If an image is connected here, its dimensions will be used as the target width and height.
- **Outputs:**
- `mask` (`MASK`): The resized mask tensor.
- `width` (`INT`): The final width of the output mask.
- `height` (`INT`): The final height of the output mask.
# 14. Arbitrary Normalize
- **Display Name:** `Arbitrary Normalize`
- **Internal Name:** `SET_NormalizeArbitrary`
- **Category:** `image/normalization`
- **Description:** Normalizes an image tensor using the mean and standard deviation provided in the `parameters` input.
- **Purpose:** Essential for pre-processing images before feeding them into models
- **Inputs:**
- `image` (`IMAGE`): A standard ComfyUI image tensor in the `[0, 1]` range.
- `parameters` (`NORM_PARAMS`): The parameters for the normalization. This is a dict with two keys, `mean` and `std`, which are lists with the R, G, B parameters.
- **Output:**
- `image` (`IMAGE`): The normalized image tensor. The value range will be altered significantly.
- **How it Works:** For each channel, it performs the operation `output = (input - mean) / std`, using the values from NORM_PARAMS
# 15. Normalize Parameters
- **Display Name:** `Normalize Parameters`
- **Internal Name:** `SET_NormalizeParameters`
- **Category:** `image/normalization`
- **Description:** Provides the `NORM_PARAMS` for the [Arbitrary Normalize](#14-arbitrary-normalize) node
- **Purpose:** This is a user interface to the [Arbitrary Normalize](#14-arbitrary-normalize) node
- **Inputs:**
- `mean_red` (`FLOAT`): Mean value for the red channel
- `mean_green` (`FLOAT`): Mean value for the green channel
- `mean_blue` (`FLOAT`): Mean value for the blue channel
- `std_red` (`FLOAT`): Standard deviation for the red channel
- `std_green` (`FLOAT`): Standard deviation for the green channel
- `std_blue` (`FLOAT`): Standard deviation for the blue channel
- **Output:**
- `parameters` (`NORM_PARAMS`): The parameters for the normalization. This is a dict with two keys, `mean` and `std`, which are lists with the R, G, B parameters.
# 16. Image with text label
- **Display Name:** `Image with text label`
- **Internal Name:** `SET_ImageWithTextLabel`
- **Category:** `image/manipulation`
- **Description:** Adds a border and a label to an image or a mask. This can be used to create comparison grids. Use the Comfy-Core `Image Stitch` node to concatenate the images.
- **Purpose:** Intended to create labeled images
- **Inputs:**
- `image` (`IMAGE`): Image to use, leave unconnected when using a mask.
- `mask` ("MASK`): Mask to be used as image, leave unconnected when using an image.
- `text` (`STRING`): Text to use as label. Can contain multiple lines.
- `side` ("top", "bottom", "left", "right"): The side where the label will be placed.
- `label_size` (`STRING`): The width (left/right) or height (top/bottom) of the area for the label. Can be expressed as a percentage (i.e. 10%) or absolute number of pixels.
- `separation` (`STRING`): The size of the border around the image. Can be expressed as a percentage (i.e. 1%) or absolute number of pixels.
- `background_color` (`STRING`): Color to fill the border and the label area. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range. Some color names are also supported.
- `foreground_color` (`STRING`): Color the label font. Can be an hexadecimal RGB (i.e. `#AABBCC`) or comma separated RGB components. The components can be in the [0-255] or [0-1.0] range. Some color names are also supported.
- `font_name` (`STRING`): The name of the font to use for the text.
- **Output:**
- `IMAGE` (`IMAGE`): The generated RGB image.
+1 -1
View File
@@ -1 +1 @@
{"id":"63a02d67-42e7-4217-9d52-28b9c456c35b","revision":0,"last_node_id":5,"last_link_id":4,"nodes":[{"id":4,"type":"MaskPreview","pos":[1457.7564697265625,1385.4490966796875],"size":[273.6515197753906,258],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":4}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"MaskPreview"},"widgets_values":[],"color":"#222","bgcolor":"#000"},{"id":3,"type":"PreviewImage","pos":[1461.320068359375,1076.2701416015625],"size":[273.6515197753906,258],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":3}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"PreviewImage"},"widgets_values":[],"color":"#222","bgcolor":"#000"},{"id":1,"type":"SET_ImageDownload","pos":[1077.0606689453125,1075.701416015625],"size":[270,314],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"image_bypass","name":"image_bypass","shape":7,"type":"IMAGE","link":1},{"localized_name":"mask_bypass","name":"mask_bypass","shape":7,"type":"MASK","link":2},{"localized_name":"base_url","name":"base_url","type":"STRING","widget":{"name":"base_url"},"link":null},{"localized_name":"filename","name":"filename","type":"STRING","widget":{"name":"filename"},"link":null}],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":[3]},{"localized_name":"alpha_mask","name":"alpha_mask","type":"MASK","links":[4]}],"properties":{"aux_id":"set-soft/ComfyUI-ImageMisc","ver":"3ab5664ee7057737c3215db19b078afbcead7a08","Node name for S&R":"SET_ImageDownload"},"widgets_values":["https://raw.githubusercontent.com/set-soft/AudioSeparation/refs/heads/main/example_workflows/","audioseparation_logo.jpg"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":2,"type":"LoadImage","pos":[727.1275024414062,1075.3787841796875],"size":[274.080078125,314],"flags":{},"order":0,"mode":4,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[1]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":[2]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"LoadImage"},"widgets_values":["example.png","image"],"color":"#222","bgcolor":"#000"},{"id":5,"type":"MarkdownNote","pos":[733.544921875,821.4869384765625],"size":[989.1290283203125,186.43255615234375],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# If you \"unbypass\" the *Load Image* node you'll get the image and mask from it\n# When the inputs are not connected or come from a bypassed node you get the downloaded image\n# Images are downloaded to the ComfyUI's *input* directory, so they become available to be selected in the *Load Image* node. Just press R to refresh the nodes.\n# To \"bypass\" a node just select it and choose the icon with an arrow around a circle.\n"],"color":"#432","bgcolor":"#653"}],"links":[[1,2,0,1,0,"IMAGE"],[2,2,1,1,1,"MASK"],[3,1,0,3,0,"IMAGE"],[4,1,1,4,0,"MASK"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8432169278238246,"offset":[-188.01505304476098,-782.3508645860205]}},"version":0.4}
{"id":"63a02d67-42e7-4217-9d52-28b9c456c35b","revision":0,"last_node_id":5,"last_link_id":4,"nodes":[{"id":4,"type":"MaskPreview","pos":[1457.7564697265625,1385.4490966796875],"size":[273.6515197753906,258],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":4}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"MaskPreview"},"widgets_values":[],"color":"#222","bgcolor":"#000"},{"id":3,"type":"PreviewImage","pos":[1461.320068359375,1076.2701416015625],"size":[273.6515197753906,258],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":3}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"PreviewImage"},"widgets_values":[],"color":"#222","bgcolor":"#000"},{"id":1,"type":"SET_ImageDownload","pos":[1077.0606689453125,1075.701416015625],"size":[270,314],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"image_bypass","name":"image_bypass","shape":7,"type":"IMAGE","link":1},{"localized_name":"mask_bypass","name":"mask_bypass","shape":7,"type":"MASK","link":2},{"localized_name":"base_url","name":"base_url","type":"STRING","widget":{"name":"base_url"},"link":null},{"localized_name":"filename","name":"filename","type":"STRING","widget":{"name":"filename"},"link":null}],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":[3]},{"localized_name":"alpha_mask","name":"alpha_mask","type":"MASK","links":[4]}],"properties":{"aux_id":"set-soft/ComfyUI-ImageMisc","ver":"3ab5664ee7057737c3215db19b078afbcead7a08","Node name for S&R":"SET_ImageDownload"},"widgets_values":["https://raw.githubusercontent.com/set-soft/AudioSeparation/refs/heads/main/example_workflows/","audioseparation_logo.jpg"],"color":"#2a363b","bgcolor":"#3f5159"},{"id":2,"type":"LoadImage","pos":[727.1275024414062,1075.3787841796875],"size":[274.080078125,314],"flags":{},"order":0,"mode":4,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[1]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":[2]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"LoadImage"},"widgets_values":["example.png","image"],"color":"#222","bgcolor":"#000"},{"id":5,"type":"MarkdownNote","pos":[733.544921875,821.4869384765625],"size":[989.1290283203125,186.43255615234375],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["# If you \"unbypass\" the *Load Image* node you'll get the image and mask from it\n# When the inputs are not connected or come from a bypassed node you get the downloaded image\n# Images are downloaded to the ComfyUI's *input* directory, so they become available to be selected in the *Load Image* node. Just press R to refresh the nodes.\n# To \"bypass\" a node just select it and choose the icon with an arrow around a circle.\n"],"color":"#432","bgcolor":"#653"}],"links":[[1,2,0,1,0,"IMAGE"],[2,2,1,1,1,"MASK"],[3,1,0,3,0,"IMAGE"],[4,1,1,4,0,"MASK"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8432169278238246,"offset":[-188.01505304476098,-782.3508645860205]}},"version":0.4}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long
+46 -3
View File
@@ -2,11 +2,20 @@
name = "image-misc"
description = """
Miscellaneous nodes for image manipulation.
Currently just download image with bypass, so you can create workflows including image examples.
Download image with bypass, so you can create workflows including image examples.
Foreground estimation and mask apply.
Face composition nodes (to reinsert a processed face).
Advanced image/mask pad/resize (from KJNodes with some additions).
Normalization nodes for low level image pre-processing.
No extra dependencies, just an internal module.
"""
dynamic = ["version"]
license = "GPL-3.0-or-later"
# This is the modern Python mechanism, but not supported
# dynamic = ["version"]
# license = "GPL-3.0-or-later"
# Only this is supported :-(
version = "1.4.0"
license = { file = "LICENSE" }
# The following is just ignored
dependencies = ["seconohe"]
[project.urls]
@@ -15,6 +24,40 @@ Repository = "https://github.com/set-soft/ComfyUI-ImageMisc"
[tool.comfy]
PublisherId = "set-soft"
DisplayName = "Image Misc"
Icon = "https://raw.githubusercontent.com/set-soft/ComfyUI-ImageMisc/main/assets/Icon_400x400.jpg"
[tool.setuptools.dynamic]
version = {attr = "nodes.__version__"}
# #######################################
# MyPy (well, yours, useless hinting)
# #######################################
[tool.mypy]
# Basic configuration
python_version = "3.11"
warn_return_any = true
warn_unused_ignores = true
mypy_path = "src/"
# This is often needed when starting out, especially with libraries that lack stubs
ignore_missing_imports = true
# You can get stricter over time by removing ignore_missing_imports
# and setting flags like these:
# disallow_untyped_defs = true
# disallow_any_unimported = true
# Tell mypy to ignore errors from third-party libraries if they are not typed
[[tool.mypy.overrides]]
module = [
"torch.*",
"torchaudio.*",
"pytest.*",
"cv2.*",
"colorama.*",
"tqdm.*",
"requests.*",
"comfy.*", # For comfy.utils
"server.*" # For server.PromptServer
]
ignore_missing_imports = true
+3
View File
@@ -0,0 +1,3 @@
Pillow
numpy
seconohe
+1 -1
View File
@@ -4,6 +4,6 @@
# Project: ComfyUI-ImageMisc
from seconohe.logger import initialize_logger
__version__ = "1.1.0"
__version__ = "1.4.0"
NODES_NAME = "ImageMisc"
main_logger = initialize_logger(NODES_NAME)
+1403 -8
View File
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
import re
import sys
from pathlib import Path
# --- Configuration ---
# The path to your pyproject.toml file, relative to the project root
PYPROJECT_PATH = Path("pyproject.toml")
# The path to the Python file containing the __version__ string
SOURCE_VERSION_PATH = Path("src/nodes/__init__.py")
# --- End Configuration ---
def get_version_from_pyproject(file_path: Path) -> str | None:
"""Extracts the version string from a pyproject.toml file."""
try:
content = file_path.read_text()
# A simple regex to find `version = "..."` under the `[project]` table
match = re.search(r'^version\s*=\s*"(.*?)"', content, re.MULTILINE)
if match:
return match.group(1)
except FileNotFoundError:
print(f"Error: {file_path} not found.", file=sys.stderr)
except Exception as e:
print(f"Error reading or parsing {file_path}: {e}", file=sys.stderr)
return None
def get_version_from_source(file_path: Path) -> str | None:
"""Extracts the __version__ string from a Python source file."""
try:
content = file_path.read_text()
# A simple regex to find `__version__ = "..."`
match = re.search(r'^__version__\s*=\s*"(.*?)"', content, re.MULTILINE)
if match:
return match.group(1)
except FileNotFoundError:
print(f"Error: {file_path} not found.", file=sys.stderr)
except Exception as e:
print(f"Error reading or parsing {file_path}: {e}", file=sys.stderr)
return None
def main() -> int:
"""
Compares version strings from pyproject.toml and the source code.
Exits with a non-zero status code if they do not match.
"""
print("--- Checking version consistency ---")
# Get versions
pyproject_version = get_version_from_pyproject(PYPROJECT_PATH)
source_version = get_version_from_source(SOURCE_VERSION_PATH)
# Validate that we found both
if not pyproject_version:
print(f"Error: Could not find version in {PYPROJECT_PATH}", file=sys.stderr)
return 1
if not source_version:
print(f"Error: Could not find `__version__` in {SOURCE_VERSION_PATH}", file=sys.stderr)
return 1
print(f"Version in {PYPROJECT_PATH}: {pyproject_version}")
print(f"Version in {SOURCE_VERSION_PATH}: {source_version}")
# Compare and exit
if pyproject_version == source_version:
print("✅ Versions are consistent.")
return 0
else:
print("\n❌ Error: Version mismatch!", file=sys.stderr)
print(f" pyproject.toml has version '{pyproject_version}'", file=sys.stderr)
print(f" {SOURCE_VERSION_PATH} has version '{source_version}'", file=sys.stderr)
print(" Please ensure both versions are identical.", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
+219
View File
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
import argparse
import os
import shutil # For file backup
import logging
# Setup logging
logger = logging.getLogger("StyleFixer")
logger.setLevel(logging.INFO) # Default level
# Create console handler and set level to debug
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG) # Let handler decide what to show based on logger's effective level
# Create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# Add formatter to ch
ch.setFormatter(formatter)
# Add ch to logger
logger.addHandler(ch)
def expand_tabs(line, tab_size=4):
"""Expands tabs to spaces, respecting tab stops."""
out_line = []
current_col = 0
for char in line:
if char == '\t':
spaces_to_add = tab_size - (current_col % tab_size)
out_line.append(' ' * spaces_to_add)
current_col += spaces_to_add
else:
out_line.append(char)
current_col += 1
return "".join(out_line)
def fix_indentation(line, tab_size=4):
"""Rounds leading whitespace indentation to the nearest multiple of tab_size."""
stripped_line = line.lstrip(' ')
if not stripped_line or stripped_line == line: # No leading spaces or empty line
return line
leading_spaces_count = len(line) - len(stripped_line)
remainder = leading_spaces_count % tab_size
if remainder == 0:
new_indent_count = leading_spaces_count
elif remainder <= tab_size / 2: # Prioritize rounding down or to current if exact multiple
new_indent_count = leading_spaces_count - remainder
else: # Round up
new_indent_count = leading_spaces_count + (tab_size - remainder)
new_indent_count = max(0, new_indent_count) # Ensure non-negative
if new_indent_count != leading_spaces_count:
logger.debug(f"Adjusting indent: from {leading_spaces_count} to {new_indent_count} for line: {line.rstrip()!r}")
return ' ' * new_indent_count + stripped_line
def process_line(line_num, line_content_with_eol, expand_tabs_enabled=True):
original_line_for_debug = line_content_with_eol # Keep for debugging if needed
line = line_content_with_eol
# 1. Expand tabs (if enabled)
if expand_tabs_enabled:
line = expand_tabs(line)
if line != original_line_for_debug and not line.isspace(): # Log only if actual content changed
logger.debug(f"L{line_num}: Tabs expanded.")
# 2. W291: Remove trailing whitespace (done before other checks that might rely on EOL)
# Also handles W293 (blank line contains whitespace) implicitly if the line becomes empty.
processed_line = line.rstrip()
if len(processed_line) < len(line.rstrip('\r\n')): # Compare lengths without EOL
logger.debug(f"L{line_num}: Trailing whitespace removed.")
# If the line became empty after rstrip, it's a truly blank line
if not processed_line and line.strip() == "":
# Return an empty string for a blank line, newline char will be added later if original had it
if line.endswith(('\n', '\r\n', '\r')):
return '\n'
return ""
# 3. Fix indentation (E111) - only if line is not blank
if processed_line:
indented_line = fix_indentation(processed_line)
if indented_line != processed_line: # Log if indentation changed
# Already logged inside fix_indentation with more detail
pass
processed_line = indented_line
# 4. E261: At least two spaces before inline comment
in_string_char = None
temp_part = ""
comment_starts_at = -1
for idx, char_val in enumerate(processed_line):
if in_string_char:
temp_part += char_val
if char_val == in_string_char:
if len(temp_part) >= 2 and temp_part[-2] == '\\':
pass
else:
in_string_char = None
elif char_val in ("'", '"'):
temp_part += char_val
in_string_char = char_val
elif char_val == '#':
comment_starts_at = idx
break # Found the first non-string comment char
else:
temp_part += char_val
if comment_starts_at != -1:
code_part = processed_line[:comment_starts_at]
comment_part = processed_line[comment_starts_at:] # Includes '#'
rstripped_code_part = code_part.rstrip()
# Only add spaces if there's actual code before the comment
if rstripped_code_part:
if not code_part.endswith(' '): # Needs fixing (less than 2 spaces)
new_line_with_comment_spacing = rstripped_code_part + ' ' + comment_part
if new_line_with_comment_spacing != processed_line:
logger.debug(f"L{line_num}: Adjusted inline comment spacing.")
processed_line = new_line_with_comment_spacing
# If rstripped_code_part is empty, it's a comment at the start of the line (after indent), leave it.
# Add back the newline character that rstrip might have removed, if the original line had one
# or if it's not an empty line that was purely whitespace
if line_content_with_eol.endswith('\n'):
return processed_line + '\n'
elif line_content_with_eol.endswith('\r\n'):
return processed_line + '\r\n'
elif line_content_with_eol.endswith('\r'):
return processed_line + '\r'
else: # Original line did not end with EOL
return processed_line
def process_file(filepath, expand_tabs_enabled=True, dry_run=False, no_backup=False):
logger.info(f"Processing {filepath}...")
try:
# Read with universal newlines mode, then splitlines to preserve EOLs correctly for rejoining
with open(filepath, 'r', encoding='utf-8', newline='') as f:
# original_content = f.read() # Reading all at once
# original_lines = original_content.splitlines(keepends=True) # This is better
original_lines = f.readlines() # Simpler, usually works fine
except Exception as e:
logger.error(f"Error reading file {filepath}: {e}")
return False
fixed_lines = []
changes_made = False
for i, line_content_with_eol in enumerate(original_lines):
fixed_line_content = process_line(i + 1, line_content_with_eol, expand_tabs_enabled)
fixed_lines.append(fixed_line_content)
if fixed_line_content != line_content_with_eol:
changes_made = True
logger.debug(f"L{i+1}: Original: {line_content_with_eol.rstrip()!r}")
logger.debug(f"L{i+1}: Fixed : {fixed_line_content.rstrip()!r}")
if changes_made:
if dry_run:
logger.info(f"Would fix style issues in {filepath} (dry run)")
else:
if not no_backup:
backup_dir = os.path.dirname(filepath)
backup_filename = "." + os.path.basename(filepath) + "~"
backup_path = os.path.join(backup_dir, backup_filename)
try:
shutil.copy2(filepath, backup_path) # copy2 preserves metadata
logger.info(f"Backup of original file created at {backup_path}")
except Exception as e:
logger.error(f"Failed to create backup for {filepath}: {e}. File not modified.")
return False
try:
# Write back lines using the original EOLs if possible, or common EOL
with open(filepath, 'w', encoding='utf-8', newline='') as f:
f.writelines(fixed_lines)
logger.info(f"Fixed style issues in {filepath}")
except Exception as e:
logger.error(f"Error writing to file {filepath}: {e}")
return False
else:
logger.info(f"No style issues to fix in {filepath}")
return True
def main():
parser = argparse.ArgumentParser(description="Fix common Python code style issues (Flake8).")
parser.add_argument("files", metavar="FILE", type=str, nargs='+',
help="Python files to process")
parser.add_argument("--no-expand-tabs", action="store_false", dest="expand_tabs",
help="Disable expansion of tabs to spaces.")
parser.add_argument("--dry-run", action="store_true",
help="Show what would be changed without modifying files.")
parser.add_argument("--no-backup", action="store_true",
help="Do not create a backup of the original file.")
parser.add_argument("-v", "--verbose", action="store_true",
help="Enable verbose (debug) logging.")
parser.set_defaults(expand_tabs=True)
args = parser.parse_args()
if args.verbose:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
for filepath in args.files:
if not os.path.isfile(filepath):
logger.warning(f"File not found: {filepath}. Skipping.")
continue
process_file(filepath, args.expand_tabs, args.dry_run, args.no_backup)
if __name__ == "__main__":
main()