diff --git a/README.md b/README.md index a058999..73d9ea5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Custom Nodes for ComfyUI +# SEB Custom Nodes for ComfyUI A collection of utility nodes for image processing, workflow management, and generation metadata tracking. @@ -13,6 +13,23 @@ A collection of utility nodes for image processing, workflow management, and gen git clone https://github.com/yourusername/seb-nodes-comfyui.git ``` 3. Restart ComfyUI. +### 1. KSampler Bridge to Text (SEB) +**Category:** `sampling/control` + +Master Controller for KSampler. Captures parameters and start time, formats them for text overlay. + +* **Inputs:** + * `latent`: The latent image. + * `seed`: Random seed. + * `steps`: Number of steps. + * `cfg`: Classifier Free Guidance scale. + * `sampler_name`: Sampler selection. + * `scheduler`: Scheduler selection. +* **Outputs:** + * `text_overlay`: Formatted string with generation info. + * `latent`, `seed`, `steps`, `cfg`, `sampler_name`, `scheduler`: Passthrough values. + * `start_time`: Timestamp for generation time calculation. +* **How to Connect:** * Connect the outputs of this node (Seed, Steps, etc.) to your actual **KSampler** node. * Connect `text_overlay` and `start_time` to the **Generation Time** node (optional). @@ -31,10 +48,33 @@ Calculates the total generation time and appends it to your text overlay. * **How to Connect:** Place this after your VAE Decode. Connect the image and the data from the Bridge node. ### 3. Text Label (SEB) -Combines multiple image batches into a single grid layout. +Adds customizable text labels to images with transparent backgrounds and alignment options. -* **Inputs:** `columns` (grid width), `padding`, and up to 6 optional image inputs (`image_1`...`image_6`). -* **Outputs:** A single grid image. +* **Inputs:** + * `image`: The input image(s). + * `text`: The text to display (multiline supported). + * `font_family`: Font selection (includes custom fonts like Funnel Sans, Google Sans Code). + * `font_size`: Size of the text. + * `text_align`: Horizontal alignment (left, center, right). + * `placement`: Position of the label (top_left, center, etc.). + * `edge_offset`: Distance from the edge. + * `color_scheme`: Preset colors (white_on_black, black_on_white). + * `padding`: Padding around the text. + * `corner_radius`: Radius of the background box corners. + * `stroke_width`: Width of the text stroke/outline. + * `background_opacity`: Opacity of the background box (0-255). +* **Outputs:** + * `image`: The image with the text label applied. + +### 4. Image Grid (SEB) +Arranges multiple image batches into a customizable grid layout. + +* **Inputs:** + * `columns`: Number of columns in the grid. + * `padding`: Padding between images in pixels. + * `image_1`...`image_6`: Optional image inputs. +* **Outputs:** + * `image`: A single grid image containing all inputs. * **Usage:** Useful for comparing results or creating contact sheets. --- diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c56544b --- /dev/null +++ b/__init__.py @@ -0,0 +1,24 @@ +""" +SEB Custom Nodes for ComfyUI +Collection of utility nodes for image processing and workflow management. +""" + +from .ksampler_control import KSamplerControl, GenerationTime +from .text_label import TextLabel +from .image_grid import ImageGrid + + +# Node Registration +NODE_CLASS_MAPPINGS = { + "KSamplerControl": KSamplerControl, + "GenerationTime": GenerationTime, + "TextLabel": TextLabel, + "ImageGrid": ImageGrid, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "KSamplerControl": "KSampler Bridge to Text (SEB)", + "GenerationTime": "Generation Time (SEB)", + "TextLabel": "Text Label (SEB)", + "ImageGrid": "Image Grid (SEB)", +} \ No newline at end of file diff --git a/fonts/Funnel_Sans/FunnelSans-Italic-VariableFont_wght.ttf b/fonts/Funnel_Sans/FunnelSans-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..58de397 Binary files /dev/null and b/fonts/Funnel_Sans/FunnelSans-Italic-VariableFont_wght.ttf differ diff --git a/fonts/Funnel_Sans/FunnelSans-VariableFont_wght.ttf b/fonts/Funnel_Sans/FunnelSans-VariableFont_wght.ttf new file mode 100644 index 0000000..2841abf Binary files /dev/null and b/fonts/Funnel_Sans/FunnelSans-VariableFont_wght.ttf differ diff --git a/fonts/Funnel_Sans/OFL.txt b/fonts/Funnel_Sans/OFL.txt new file mode 100644 index 0000000..17261bd --- /dev/null +++ b/fonts/Funnel_Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Funnel Project Authors (https://github.com/Dicotype/Funnel) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/fonts/Funnel_Sans/README.txt b/fonts/Funnel_Sans/README.txt new file mode 100644 index 0000000..0be7f65 --- /dev/null +++ b/fonts/Funnel_Sans/README.txt @@ -0,0 +1,75 @@ +Funnel Sans Variable Font +========================= + +This download contains Funnel Sans as both variable fonts and static fonts. + +Funnel Sans is a variable font with this axis: + wght + +This means all the styles are contained in these files: + Funnel_Sans/FunnelSans-VariableFont_wght.ttf + Funnel_Sans/FunnelSans-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Funnel Sans: + Funnel_Sans/static/FunnelSans-Light.ttf + Funnel_Sans/static/FunnelSans-Regular.ttf + Funnel_Sans/static/FunnelSans-Medium.ttf + Funnel_Sans/static/FunnelSans-SemiBold.ttf + Funnel_Sans/static/FunnelSans-Bold.ttf + Funnel_Sans/static/FunnelSans-ExtraBold.ttf + Funnel_Sans/static/FunnelSans-LightItalic.ttf + Funnel_Sans/static/FunnelSans-Italic.ttf + Funnel_Sans/static/FunnelSans-MediumItalic.ttf + Funnel_Sans/static/FunnelSans-SemiBoldItalic.ttf + Funnel_Sans/static/FunnelSans-BoldItalic.ttf + Funnel_Sans/static/FunnelSans-ExtraBoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/fonts/Funnel_Sans/static/FunnelSans-Bold.ttf b/fonts/Funnel_Sans/static/FunnelSans-Bold.ttf new file mode 100644 index 0000000..7146dc6 Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-Bold.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-BoldItalic.ttf b/fonts/Funnel_Sans/static/FunnelSans-BoldItalic.ttf new file mode 100644 index 0000000..8f2546b Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-BoldItalic.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-ExtraBold.ttf b/fonts/Funnel_Sans/static/FunnelSans-ExtraBold.ttf new file mode 100644 index 0000000..b82027a Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-ExtraBold.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-ExtraBoldItalic.ttf b/fonts/Funnel_Sans/static/FunnelSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000..711dfce Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-ExtraBoldItalic.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-Italic.ttf b/fonts/Funnel_Sans/static/FunnelSans-Italic.ttf new file mode 100644 index 0000000..8274598 Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-Italic.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-Light.ttf b/fonts/Funnel_Sans/static/FunnelSans-Light.ttf new file mode 100644 index 0000000..9d05065 Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-Light.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-LightItalic.ttf b/fonts/Funnel_Sans/static/FunnelSans-LightItalic.ttf new file mode 100644 index 0000000..afc2d59 Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-LightItalic.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-Medium.ttf b/fonts/Funnel_Sans/static/FunnelSans-Medium.ttf new file mode 100644 index 0000000..29a014e Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-Medium.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-MediumItalic.ttf b/fonts/Funnel_Sans/static/FunnelSans-MediumItalic.ttf new file mode 100644 index 0000000..ba25a24 Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-MediumItalic.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-Regular.ttf b/fonts/Funnel_Sans/static/FunnelSans-Regular.ttf new file mode 100644 index 0000000..3e497ba Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-Regular.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-SemiBold.ttf b/fonts/Funnel_Sans/static/FunnelSans-SemiBold.ttf new file mode 100644 index 0000000..195dc97 Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-SemiBold.ttf differ diff --git a/fonts/Funnel_Sans/static/FunnelSans-SemiBoldItalic.ttf b/fonts/Funnel_Sans/static/FunnelSans-SemiBoldItalic.ttf new file mode 100644 index 0000000..934ac9c Binary files /dev/null and b/fonts/Funnel_Sans/static/FunnelSans-SemiBoldItalic.ttf differ diff --git a/fonts/Google_Sans_Code/GoogleSansCode-Italic-VariableFont_wght.ttf b/fonts/Google_Sans_Code/GoogleSansCode-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..294fd5b Binary files /dev/null and b/fonts/Google_Sans_Code/GoogleSansCode-Italic-VariableFont_wght.ttf differ diff --git a/fonts/Google_Sans_Code/GoogleSansCode-VariableFont_wght.ttf b/fonts/Google_Sans_Code/GoogleSansCode-VariableFont_wght.ttf new file mode 100644 index 0000000..b68d037 Binary files /dev/null and b/fonts/Google_Sans_Code/GoogleSansCode-VariableFont_wght.ttf differ diff --git a/fonts/Google_Sans_Code/OFL.txt b/fonts/Google_Sans_Code/OFL.txt new file mode 100644 index 0000000..3b28393 --- /dev/null +++ b/fonts/Google_Sans_Code/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2025 The Google Sans Code Project Authors (github.com/googlefonts/googlesans-code) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/fonts/Google_Sans_Code/README.txt b/fonts/Google_Sans_Code/README.txt new file mode 100644 index 0000000..ed8a516 --- /dev/null +++ b/fonts/Google_Sans_Code/README.txt @@ -0,0 +1,75 @@ +Google Sans Code Variable Font +============================== + +This download contains Google Sans Code as both variable fonts and static fonts. + +Google Sans Code is a variable font with this axis: + wght + +This means all the styles are contained in these files: + Google_Sans_Code/GoogleSansCode-VariableFont_wght.ttf + Google_Sans_Code/GoogleSansCode-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Google Sans Code: + Google_Sans_Code/static/GoogleSansCode-Light.ttf + Google_Sans_Code/static/GoogleSansCode-Regular.ttf + Google_Sans_Code/static/GoogleSansCode-Medium.ttf + Google_Sans_Code/static/GoogleSansCode-SemiBold.ttf + Google_Sans_Code/static/GoogleSansCode-Bold.ttf + Google_Sans_Code/static/GoogleSansCode-ExtraBold.ttf + Google_Sans_Code/static/GoogleSansCode-LightItalic.ttf + Google_Sans_Code/static/GoogleSansCode-Italic.ttf + Google_Sans_Code/static/GoogleSansCode-MediumItalic.ttf + Google_Sans_Code/static/GoogleSansCode-SemiBoldItalic.ttf + Google_Sans_Code/static/GoogleSansCode-BoldItalic.ttf + Google_Sans_Code/static/GoogleSansCode-ExtraBoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-Bold.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-Bold.ttf new file mode 100644 index 0000000..e42ea23 Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-Bold.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-BoldItalic.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-BoldItalic.ttf new file mode 100644 index 0000000..6a32258 Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-BoldItalic.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-ExtraBold.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-ExtraBold.ttf new file mode 100644 index 0000000..11f08d5 Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-ExtraBold.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-ExtraBoldItalic.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-ExtraBoldItalic.ttf new file mode 100644 index 0000000..abd4fe6 Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-ExtraBoldItalic.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-Italic.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-Italic.ttf new file mode 100644 index 0000000..789fa3f Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-Italic.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-Light.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-Light.ttf new file mode 100644 index 0000000..714fb31 Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-Light.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-LightItalic.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-LightItalic.ttf new file mode 100644 index 0000000..245c193 Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-LightItalic.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-Medium.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-Medium.ttf new file mode 100644 index 0000000..5e7f46b Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-Medium.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-MediumItalic.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-MediumItalic.ttf new file mode 100644 index 0000000..821e8fa Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-MediumItalic.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-Regular.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-Regular.ttf new file mode 100644 index 0000000..5c520ad Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-Regular.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-SemiBold.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-SemiBold.ttf new file mode 100644 index 0000000..a03c7f0 Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-SemiBold.ttf differ diff --git a/fonts/Google_Sans_Code/static/GoogleSansCode-SemiBoldItalic.ttf b/fonts/Google_Sans_Code/static/GoogleSansCode-SemiBoldItalic.ttf new file mode 100644 index 0000000..95efb6f Binary files /dev/null and b/fonts/Google_Sans_Code/static/GoogleSansCode-SemiBoldItalic.ttf differ diff --git a/fonts/arial.ttf b/fonts/arial.ttf new file mode 100644 index 0000000..810df57 Binary files /dev/null and b/fonts/arial.ttf differ diff --git a/fonts/arialbd.ttf b/fonts/arialbd.ttf new file mode 100644 index 0000000..22040fc Binary files /dev/null and b/fonts/arialbd.ttf differ diff --git a/fonts/consola.ttf b/fonts/consola.ttf new file mode 100644 index 0000000..e881ca4 Binary files /dev/null and b/fonts/consola.ttf differ diff --git a/image_grid.py b/image_grid.py new file mode 100644 index 0000000..894dcb0 --- /dev/null +++ b/image_grid.py @@ -0,0 +1,135 @@ +""" +Image Grid Node for ComfyUI +Arranges multiple image batches into a customizable grid layout. +""" + +import math +from typing import Optional, Tuple + +import torch +import torch.nn.functional as F + + +class ImageGrid: + """ComfyUI node for creating image grids from multiple image batches.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "columns": ("INT", {"default": 2, "min": 0, "max": 100, "step": 1}), + "padding": ("INT", {"default": 2, "min": 0, "max": 500, "step": 1}), + }, + "optional": { + "image_1": ("IMAGE",), + "image_2": ("IMAGE",), + "image_3": ("IMAGE",), + "image_4": ("IMAGE",), + "image_5": ("IMAGE",), + "image_6": ("IMAGE",), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "create_grid" + CATEGORY = "image/grid" + + def create_grid( + self, + columns: int, + padding: int, + image_1: Optional[torch.Tensor] = None, + image_2: Optional[torch.Tensor] = None, + image_3: Optional[torch.Tensor] = None, + image_4: Optional[torch.Tensor] = None, + image_5: Optional[torch.Tensor] = None, + image_6: Optional[torch.Tensor] = None + ) -> Tuple[torch.Tensor]: + """ + Create a grid from multiple image batches. + + Args: + columns: Number of columns in the grid. + padding: Padding between images in pixels. + image_1...image_6: Optional image batches to include in the grid. + + Returns: + A single tensor containing the grid image. + """ + # Collect all provided images + all_images = [ + img for img in [image_1, image_2, image_3, image_4, image_5, image_6] + if img is not None + ] + + if not all_images: + return (torch.zeros((1, 512, 512, 3)),) + + # Get base dimensions from first image batch + base_shape = all_images[0].shape + target_height = base_shape[1] + target_width = base_shape[2] + channels = base_shape[3] + + # Resize all images to match first image dimensions + processed_images = [] + for img_batch in all_images: + if img_batch.shape[1] != target_height or img_batch.shape[2] != target_width: + # Resize: permute to [B, C, H, W] for interpolation + img_permuted = img_batch.permute(0, 3, 1, 2) + img_resized = F.interpolate( + img_permuted, + size=(target_height, target_width), + mode="bilinear", + align_corners=False + ) + # Permute back to [B, H, W, C] + img_batch = img_resized.permute(0, 2, 3, 1) + + processed_images.append(img_batch) + + # Concatenate all image batches + final_images = torch.cat(processed_images, dim=0) + batch_size = final_images.shape[0] + + if batch_size == 0: + return (torch.zeros((1, target_height, target_width, channels)),) + + # Calculate grid dimensions + if columns > 0: + cols = columns + rows = math.ceil(batch_size / cols) + else: + # Auto-calculate roughly square grid + cols = math.ceil(math.sqrt(batch_size)) + rows = math.ceil(batch_size / cols) + + # Calculate output dimensions + grid_height = rows * target_height + (rows - 1) * padding + grid_width = cols * target_width + (cols - 1) * padding + + # Initialize grid with zeros (black background) + grid = torch.zeros( + (1, grid_height, grid_width, channels), + dtype=final_images.dtype, + device=final_images.device + ) + + # Place images in grid + for idx in range(batch_size): + row = idx // cols + col = idx % cols + + y_start = row * (target_height + padding) + y_end = y_start + target_height + x_start = col * (target_width + padding) + x_end = x_start + target_width + + grid[0, y_start:y_end, x_start:x_end, :] = final_images[idx] + + return (grid,) + + +NODE_CLASS_MAPPINGS = {"ImageGrid": ImageGrid} +NODE_DISPLAY_NAME_MAPPINGS = {"ImageGrid": "Image Grid (SEB)"} diff --git a/ksampler_control.py b/ksampler_control.py new file mode 100644 index 0000000..d7abde9 --- /dev/null +++ b/ksampler_control.py @@ -0,0 +1,155 @@ +""" +KSampler Control Nodes for ComfyUI +Nodes for managing KSampler parameters and tracking generation time. +""" + +import time +from typing import Tuple, Dict, Any + +import torch +import comfy.samplers + + +class KSamplerControl: + """ + Master Controller for KSampler. + Captures parameters and start time, formats them for text overlay. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "latent": ("LATENT",), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "steps": ("INT", {"default": 9, "min": 1, "max": 10000}), + "cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 100.0, "step": 0.1}), + "sampler_name": (comfy.samplers.KSampler.SAMPLERS,), + "scheduler": (comfy.samplers.KSampler.SCHEDULERS,), + } + } + + RETURN_TYPES = ( + "STRING", + "LATENT", + "INT", + "INT", + "FLOAT", + comfy.samplers.KSampler.SAMPLERS, + comfy.samplers.KSampler.SCHEDULERS, + "FLOAT" + ) + RETURN_NAMES = ( + "text_overlay", + "latent", + "seed", + "steps", + "cfg", + "sampler_name", + "scheduler", + "start_time" + ) + + FUNCTION = "control" + CATEGORY = "sampling/control" + + def control( + self, + latent: Dict[str, Any], + seed: int, + steps: int, + cfg: float, + sampler_name: str, + scheduler: str + ) -> Tuple[str, Dict[str, Any], int, int, float, str, str, float]: + """ + Capture KSampler parameters and format them for display. + + Args: + latent: The latent image dictionary. + seed: Random seed used for generation. + steps: Number of sampling steps. + cfg: Classifier Free Guidance scale. + sampler_name: Name of the sampler. + scheduler: Name of the scheduler. + + Returns: + Tuple containing text overlay string, passed-through parameters, and start time. + """ + # Capture start time for generation tracking + start_time = time.time() + + # Extract resolution from latent tensor + # Latent shape: [batch, channels, height, width] + latent_samples = latent["samples"] + latent_height = latent_samples.shape[2] + latent_width = latent_samples.shape[3] + + # Calculate actual image resolution (VAE uses 8x downsampling) + width = latent_width * 8 + height = latent_height * 8 + + # Format text overlay + text = ( + f"Seed: {seed}\n" + f"Steps: {steps} | CFG: {cfg:.1f}\n" + f"{sampler_name} | {scheduler}\n" + f"Resolution: {width}x{height}" + ) + + return (text, latent, seed, steps, cfg, sampler_name, scheduler, start_time) + + +class GenerationTime: + """ + Appends generation time to text overlay. + Should be placed AFTER image generation is complete. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "text": ("STRING", {"forceInput": True}), + "start_time": ("FLOAT", {"forceInput": True}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "text") + FUNCTION = "append_time" + CATEGORY = "sampling/control" + + def append_time( + self, + image: torch.Tensor, + text: str, + start_time: float + ) -> Tuple[torch.Tensor, str]: + """ + Calculate and append generation time to text. + + Args: + image: The generated image tensor. + text: The existing text overlay string. + start_time: The start time timestamp. + + Returns: + Tuple containing the image and the updated text string. + """ + end_time = time.time() + duration = end_time - start_time + new_text = f"{text}\nTime: {duration:.2f}s" + return (image, new_text) + + +NODE_CLASS_MAPPINGS = { + "KSamplerControl": KSamplerControl, + "GenerationTime": GenerationTime, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "KSamplerControl": "KSampler Bridge to Text (SEB)", + "GenerationTime": "Generation Time (SEB)", +} diff --git a/text_label.py b/text_label.py new file mode 100644 index 0000000..17e6754 --- /dev/null +++ b/text_label.py @@ -0,0 +1,294 @@ +""" +Text Label Node for ComfyUI +Adds customizable text labels to images with transparent backgrounds and alignment options. +""" + +import os +import platform +from typing import Tuple, List + +import numpy as np +import torch +from PIL import Image, ImageDraw, ImageFont + + +# Define filenames for custom fonts (relative to 'fonts' directory) +CUSTOM_FONTS = { + "Funnel Sans": [ + "Funnel_Sans/static/FunnelSans-Regular.ttf", + "FunnelSans-Regular.ttf" + ], + "Funnel Sans Bold": [ + "Funnel_Sans/static/FunnelSans-Bold.ttf", + "FunnelSans-Bold.ttf" + ], + "Google Sans Code": [ + "Google_Sans_Code/static/GoogleSansCode-Regular.ttf", + "GoogleSansCode-Regular.ttf" + ], + "Google Sans Code Bold": [ + "Google_Sans_Code/static/GoogleSansCode-Bold.ttf", + "GoogleSansCode-Bold.ttf" + ], + "Arial": ["arial.ttf", "Arial.ttf"], + "Arial Bold": ["arialbd.ttf", "Arialbd.ttf", "Arial Bold.ttf"], + "Monospace": ["consola.ttf", "Consola.ttf", "DejaVuSansMono.ttf"], +} + + +# Define system fallback paths +SYSTEM_FONTS = { + "Arial": [ + "C:\\Windows\\Fonts\\arial.ttf", + "/Library/Fonts/Arial.ttf", + "/usr/share/fonts/truetype/msttcorefonts/Arial.ttf" + ], + "Arial Bold": [ + "C:\\Windows\\Fonts\\arialbd.ttf", + "/Library/Fonts/Arial Bold.ttf", + "/usr/share/fonts/truetype/msttcorefonts/Arial_Bold.ttf" + ], + "Monospace": [ + "C:\\Windows\\Fonts\\consola.ttf", + "/Library/Fonts/Menlo.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf" + ], +} + + +def hex_to_rgba(hex_color: str, alpha: float = 1.0) -> Tuple[int, int, int, int]: + """Convert hex color string to RGBA tuple.""" + hex_color = hex_color.strip().lstrip("#") + + if len(hex_color) == 3: + r, g, b = [int(c * 2, 16) for c in hex_color] + elif len(hex_color) == 6: + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + else: + r, g, b = 255, 255, 255 + + alpha_value = max(0, min(255, int(alpha * 255))) + return (r, g, b, alpha_value) + + +def tensor_to_pil(tensor: torch.Tensor) -> Image.Image: + """Convert PyTorch tensor to PIL Image.""" + array = (tensor.clamp(0, 1).cpu().numpy() * 255.0).round().astype(np.uint8) + return Image.fromarray(array) + + +def pil_to_tensor(image: Image.Image) -> torch.Tensor: + """Convert PIL Image to PyTorch tensor.""" + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0) + + +def get_font_paths(font_family: str) -> List[str]: + """Get list of possible font file paths for the given font family.""" + paths = [] + + if font_family in CUSTOM_FONTS: + filenames = CUSTOM_FONTS[font_family] + # Add local fonts directory + local_fonts_dir = os.path.join(os.path.dirname(__file__), "fonts") + for fname in filenames: + paths.append(fname) # Try direct filename (system/CWD) + paths.append(os.path.join(local_fonts_dir, fname)) # Try local folder + + if font_family in SYSTEM_FONTS: + paths.extend(SYSTEM_FONTS[font_family]) + + # Add user font directories on Linux + if platform.system().lower() == "linux": + home = os.path.expanduser("~") + paths += [ + os.path.join(home, ".fonts", "DejaVuSansMono.ttf"), + os.path.join(home, ".local", "share", "fonts", "DejaVuSansMono.ttf") + ] + + return paths + + +def load_font(font_family: str, font_size: int): + """Load font from system or return default font.""" + for font_path in get_font_paths(font_family): + try: + return ImageFont.truetype(font_path, font_size) + except Exception: + continue + + return ImageFont.load_default() + + +def get_text_bbox(draw: ImageDraw.ImageDraw, text: str, font) -> Tuple[int, int, int, int]: + """Get bounding box of text.""" + try: + return draw.textbbox((0, 0), text, font=font) + except Exception: + width = int(draw.textlength(text, font=font)) + height = int(font.size) + return (0, 0, width, height) + + +def calculate_position( + image_width: int, + image_height: int, + box_width: int, + box_height: int, + placement: str, + edge_offset: int +) -> Tuple[int, int]: + """Calculate x, y coordinates for label placement.""" + positions = { + "top_left": (edge_offset, edge_offset), + "top_right": (image_width - box_width - edge_offset, edge_offset), + "bottom_left": (edge_offset, image_height - box_height - edge_offset), + "bottom_right": (image_width - box_width - edge_offset, image_height - box_height - edge_offset), + "center": ((image_width - box_width) // 2, (image_height - box_height) // 2) + } + + x, y = positions.get(placement, positions["center"]) + return int(x), int(y) + + +class TextLabel: + """ComfyUI node for adding text labels to images.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "text": ("STRING", {"default": "YOUR TEXT", "multiline": True}), + }, + "optional": { + "font_family": ( + [ + "Arial", "Arial Bold", "Monospace", + "Funnel Sans", "Funnel Sans Bold", + "Google Sans Code", "Google Sans Code Bold" + ], + {"default": "Google Sans Code Bold"} + ), + "font_size": ("INT", {"default": 30, "min": 6, "max": 256, "step": 1}), + "text_align": (["left", "center", "right"], {"default": "left"}), + "placement": ( + ["top_left", "top_right", "bottom_left", "bottom_right", "center"], + {"default": "top_left"} + ), + "edge_offset": ("INT", {"default": 25, "min": 0, "max": 4096, "step": 1}), + "color_scheme": ( + ["white_on_black", "black_on_white"], + {"default": "white_on_black"} + ), + "padding": ("INT", {"default": 18, "min": 0, "max": 256, "step": 1}), + "corner_radius": ("INT", {"default": 15, "min": 0, "max": 128, "step": 1}), + "stroke_width": ("INT", {"default": 1, "min": 0, "max": 20, "step": 1}), + "background_opacity": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}), + }, + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "add_text_label" + CATEGORY = "image/annotation" + + def add_text_label( + self, + image: torch.Tensor, + text: str, + font_family: str = "Arial", + font_size: int = 30, + text_align: str = "center", + placement: str = "top_left", + edge_offset: int = 30, + color_scheme: str = "black_on_white", + padding: int = 15, + corner_radius: int = 15, + stroke_width: int = 0, + background_opacity: int = 0, + ) -> Tuple[torch.Tensor]: + """Add text label to image(s).""" + # Ensure image is 4D (batch, height, width, channels) + if image.dim() == 3: + image = image.unsqueeze(0) + + # Load font + font = load_font(font_family, font_size) + + # Set text and background colors + if color_scheme == "white_on_black": + text_color, bg_color = "#FFFFFF", "#000000" + stroke_color = "#000000" + else: + text_color, bg_color = "#000000", "#FFFFFF" + stroke_color = "#FFFFFF" + + # Process each image in batch + output_images = [] + for i in range(image.shape[0]): + # Convert to PIL Image + pil_image = tensor_to_pil(image[i]).convert("RGBA") + draw = ImageDraw.Draw(pil_image, "RGBA") + + # Measure text dimensions + left, top, right, bottom = get_text_bbox(draw, text, font) + text_width = right - left + text_height = bottom - top + + # Calculate box dimensions + box_width = text_width + padding * 2 + box_height = text_height + padding * 2 + + # Calculate label position + x, y = calculate_position( + pil_image.width, + pil_image.height, + box_width, + box_height, + placement, + edge_offset + ) + + # Create overlay for semi-transparent background + overlay = Image.new("RGBA", pil_image.size, (0, 0, 0, 0)) + overlay_draw = ImageDraw.Draw(overlay, "RGBA") + + # Draw rounded rectangle background with 50% opacity + radius = max(0, min(corner_radius, min(box_width, box_height) // 2)) + overlay_draw.rounded_rectangle( + [x, y, x + box_width, y + box_height], + radius=radius, + fill=hex_to_rgba(bg_color, background_opacity / 255.0) + ) + + # Composite the overlay onto the base image + pil_image = Image.alpha_composite(pil_image, overlay) + + # Draw text with full opacity and alignment + draw = ImageDraw.Draw(pil_image, "RGBA") + + # Center text within the box + center_x = x + box_width / 2.0 + center_y = y + box_height / 2.0 + text_x = int(center_x - text_width / 2.0 - left) + text_y = int(center_y - text_height / 2.0 - top) + + draw.multiline_text( + (text_x, text_y), + text, + font=font, + fill=hex_to_rgba(text_color, 1.0), + align=text_align, + stroke_width=stroke_width, + stroke_fill=hex_to_rgba(stroke_color, 1.0) + ) + + # Convert back to tensor + output_images.append(pil_to_tensor(pil_image.convert("RGB"))) + + return (torch.stack(output_images, dim=0),) + + +NODE_CLASS_MAPPINGS = {"TextLabel": TextLabel} +NODE_DISPLAY_NAME_MAPPINGS = {"TextLabel": "Text Label (SEB)"}