Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f17e718fd | ||
|
|
065adbd8d0 | ||
|
|
028eda753c | ||
|
|
3cc287ddc7 | ||
|
|
ea768fa93b | ||
|
|
1b93cde829 | ||
|
|
7053dfc72b | ||
|
|
9afd74f4d6 | ||
|
|
3e9dcbb67f | ||
|
|
ae7a0923f6 | ||
|
|
ffab79b7fc | ||
|
|
0c64e93180 | ||
|
|
26f188f0e0 | ||
|
|
a73d04dfe8 | ||
|
|
5d7377287e | ||
|
|
95e929863d | ||
|
|
a0eeebbfbf | ||
|
|
f93908247d | ||
|
|
2e566fc2e8 | ||
|
|
93f28874d5 | ||
|
|
d7708c7cd9 | ||
|
|
6d1d321691 | ||
|
|
e6441bbae3 | ||
|
|
76c82b7af5 | ||
|
|
03b0096be6 | ||
|
|
86f8686d30 | ||
|
|
63253a9267 | ||
|
|
28f8d58532 | ||
|
|
1dd948580c | ||
|
|
a58e0bf17d | ||
|
|
269e3a4f5b | ||
|
|
95dc2adf9a | ||
|
|
b02167cfda | ||
|
|
58f79cf91f | ||
|
|
89992997c5 | ||
|
|
67d3c6d9d4 | ||
|
|
29e0bbdad6 | ||
|
|
dbf6449949 | ||
|
|
405e4b4d19 | ||
|
|
22b183168d | ||
|
|
6f690c48f7 | ||
|
|
824b888222 | ||
|
|
d3d9bc6cff | ||
|
|
23a4bb6a79 | ||
|
|
bcfb465380 |
@@ -1 +1,2 @@
|
||||
/__pycache__
|
||||
font_cache.json
|
||||
|
||||
@@ -6,113 +6,460 @@
|
||||
|
||||
**Originally inspired by:** [mikkel/ComfyUI-text-overlay](https://github.com/mikkel/ComfyUI-text-overlay)
|
||||
|
||||
This node extends and refines the classic ComfyUI Text Overlay concept with a **re-engineered rendering engine**, **precise alignment logic**, **new customization parameters**, and **enhanced batch handling** — making it the most advanced text overlay solution available for ComfyUI.
|
||||
This module provides the **most feature‑rich and precise text overlay system available for ComfyUI**, including:
|
||||
|
||||
* Completely re‑engineered layout engine
|
||||
* Pixel‑perfect multiline stroke alignment
|
||||
* Opacity‑aware backgrounds and shadows
|
||||
* Inline rich text with HTML-like tags for bold, italic, per-segment text color, and per-segment highlight backgrounds
|
||||
* Letter/line spacing, padding, and alignment controls
|
||||
* Full animation engine (fade + directional movement)
|
||||
* Batch-aware rendering with smart caching
|
||||
* Full video processing node with audio‑preserving re‑mux
|
||||
* Font dropdown with system font discovery
|
||||
- Automatically detects installed fonts on Windows, macOS, and Linux
|
||||
- Cross-platform font scanning
|
||||
- Fallback to common font names for compatibility
|
||||
- Reusable font system for future nodes
|
||||
* **NEW: Instagram Question Box nodes** — render a two-panel sticker overlay (dark header + light body) matching the Instagram "Ask me a question" story format, with full typography, color, shadow, and animation controls
|
||||
|
||||
|
||||
---
|
||||

|
||||
## ✨ Key Features
|
||||
|
||||
## ✨ Key Improvements & New Features
|
||||
### ✔️ 1. Rebuilt Rendering Engine
|
||||
|
||||
### 🧱 1. Rebuilt Rendering Engine
|
||||
* Accurate bounding boxes for multiline text
|
||||
* Consistent stroke widths (even‑pixel enforcement)
|
||||
* Crisp, stable text edges with alpha‑aware compositing
|
||||
|
||||
* Fully restructured text rendering logic for higher precision and cleaner anti-aliasing.
|
||||
* Pixel-accurate bounding box calculations ensure text aligns perfectly to the intended position.
|
||||
* Consistent stroke behavior across font sizes with improved blending and color stability.
|
||||
### ✔️ 2. Advanced Layout and Typography
|
||||
|
||||
### 🎨 2. Extended Styling and Layout Control
|
||||
* **Letter spacing** and **line spacing**
|
||||
* **Padding** around the text block
|
||||
* Horizontal and vertical alignment: `left` / `center` / `right`, `top` / `middle` / `bottom`
|
||||
* **Per‑axis offset** (`x_shift`, `y_shift`)
|
||||
* Automatic multiline wrapping based on image width
|
||||
|
||||
* **Line spacing**, **letter spacing** and **padding** parameters for advanced multi-line composition.
|
||||
* **Independent X/Y shifting** for fine-tuned manual positioning.
|
||||
* **Full hex color control** for both text fill and stroke.
|
||||
* Supports transparent overlays for non-destructive compositing.
|
||||
### ✔️ 3. Styling Options
|
||||
|
||||
### 🧭 3. Smarter Alignment and Positioning
|
||||
* Fill + alpha
|
||||
* Stroke + alpha
|
||||
* Shadow + alpha + offset
|
||||
* Rounded background box with padding, color, radius, and alpha
|
||||
* Inline rich text styling with HTML-like tags
|
||||
|
||||
* Expanded support for:
|
||||
### ✔️ 4. Inline Rich Text
|
||||
|
||||
* Horizontal alignment: `left`, `center`, `right`
|
||||
* Vertical alignment: `top`, `middle`, `bottom`
|
||||
* Reliable anchor-based alignment even on varying resolutions and batch images.
|
||||
The `text` field now supports a small HTML-like subset for styling parts of the text independently.
|
||||
|
||||
### 🔠 4. Font Management
|
||||
Supported tags:
|
||||
|
||||
* Custom fonts can be loaded directly from the repo’s `/fonts` directory.
|
||||
* Supports both `.ttf` and `.otf` formats.
|
||||
* Graceful fallback to a system font if a specified font is unavailable.
|
||||
* `<b>bold</b>`
|
||||
* `<i>italic</i>`
|
||||
* `<br>` for line breaks
|
||||
* `<span color="#FF0000">red text</span>`
|
||||
* `<span bg="#000000">highlighted text</span>`
|
||||
* `<span align="center">centered line block</span>`
|
||||
* `<div align="right">right-aligned line block</div>`
|
||||
* `<span style="color:#00FF00; background-color:#222222">combined styling</span>`
|
||||
|
||||
### ⚙️ 5. Batch and Workflow Enhancements
|
||||
Supported attributes on `<span>`:
|
||||
|
||||
* Native **batch support** — automatically applies consistent overlay logic across all input images.
|
||||
* Optimized performance and memory use when processing large image sets.
|
||||
* Node parameters persist and preview correctly in ComfyUI sessions.
|
||||
| Attribute | Purpose |
|
||||
| --------- | ------- |
|
||||
| `color` | Per-segment text color |
|
||||
| `fill` | Alias for text color |
|
||||
| `fg` | Alias for text color |
|
||||
| `bg` | Per-segment highlight/background color |
|
||||
| `background` | Alias for background color |
|
||||
| `background-color` | Alias for background color |
|
||||
| `align` | Overrides line alignment for that styled portion (`left`, `center`, `right`) |
|
||||
| `style` | Inline CSS-like support for `color` and `background-color` |
|
||||
|
||||
### 🧩 6. Developer & Extensibility Features
|
||||
Examples:
|
||||
|
||||
* Modularized code for easier maintenance and feature additions.
|
||||
* Clean separation between text layout and rendering logic.
|
||||
* Ready for further extensions such as shadow layers, gradient text, or dynamic variable injection.
|
||||
```html
|
||||
Hello <b>bold</b> and <i>italic</i><br>
|
||||
<span color="#ff0000">red</span> and <span color="#00aaff" bg="#111111">blue on dark background</span>
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
* This is **not full HTML/CSS rendering** — it is a safe, limited inline styling system.
|
||||
* Plain text still works exactly as before.
|
||||
* Bold and italic use best-effort font variant matching. If the selected font has no matching variant, the regular font is used.
|
||||
* Inline background highlights use the node's `bg_alpha` / `bg_radius` settings for opacity and rounding.
|
||||
* Inline alignment overrides the node's `font_alignment` for the wrapped portion only.
|
||||
* Invalid inline colors fall back to the node's normal `fill_color_hex` or `bg_color_hex` values.
|
||||
* Unsupported HTML tags are ignored for styling; their text content still renders.
|
||||
|
||||
### ✔️ 5. Rich Text Examples
|
||||
|
||||
#### Mixed emphasis
|
||||
|
||||
```html
|
||||
This is <b>bold</b>, <i>italic</i>, and <b><i>both</i></b>.
|
||||
```
|
||||
|
||||
#### Multi-color text
|
||||
|
||||
```html
|
||||
<span color="#ff5555">Red</span>
|
||||
<span color="#55ff55">Green</span>
|
||||
<span color="#5599ff">Blue</span>
|
||||
```
|
||||
|
||||
#### Highlighted words only
|
||||
|
||||
```html
|
||||
Normal text with <span bg="#6b1d1d">highlighted words</span> in the middle.
|
||||
```
|
||||
|
||||
#### Combined color + highlight + emphasis
|
||||
|
||||
```html
|
||||
<span color="#ffffff" bg="#004488"><b>Important label</b></span>
|
||||
```
|
||||
|
||||
#### Using inline style
|
||||
|
||||
```html
|
||||
<span style="color:#ffd700; background-color:#202020">Styled with inline attributes</span>
|
||||
```
|
||||
|
||||
#### Multiline rich text
|
||||
|
||||
```html
|
||||
Title: <b>Episode 01</b><br>
|
||||
<span color="#cccccc">Subtitle line</span><br>
|
||||
<span color="#ffffff" bg="#7a0018">Now Playing</span>
|
||||
```
|
||||
|
||||
#### Alignment override inside a left-aligned node
|
||||
|
||||
```html
|
||||
Intro line<br>
|
||||
<span align="center"><b>Centered title</b></span><br>
|
||||
Ending line
|
||||
```
|
||||
|
||||
#### Right-aligned callout block
|
||||
|
||||
```html
|
||||
<div align="right"><span bg="#222222" color="#ffffff">Right callout</span></div>
|
||||
```
|
||||
|
||||
### ✔️ 6. Animation System
|
||||
|
||||
Supports:
|
||||
|
||||
* `fade_in`
|
||||
* `fade_out`
|
||||
* `move_from_top`
|
||||
* `move_from_bottom`
|
||||
* `move_from_left`
|
||||
* `move_from_right`
|
||||
|
||||
With easing:
|
||||
|
||||
* `linear`
|
||||
* `ease_in`
|
||||
* `ease_out`
|
||||
* `ease_in_out`
|
||||
|
||||
Animation Parameters:
|
||||
|
||||
* `animation_frames`
|
||||
* `animation_opacity_target`
|
||||
* `pause_frames_before_start`
|
||||
|
||||
### ✔️ 7. Batch-Smart Processing
|
||||
|
||||
* Automatically caches layout on first frame
|
||||
* Maintains perfect consistency across all frames
|
||||
* For animated batches: animates through first *N* frames, then holds the final pose
|
||||
|
||||
### ✔️ 8. Full Video Support
|
||||
|
||||
The **Advanced Text Overlay – Video** node:
|
||||
|
||||
* Reads a video frame‑by‑frame
|
||||
* Applies the same overlay logic
|
||||
* Writes a new MP4
|
||||
* **Automatically re‑injects the original audio track with ffmpeg**
|
||||
* Optional `delete_original` flag
|
||||
|
||||
### ✔️ 9. Instagram Question Box
|
||||
|
||||
Two dedicated nodes — **`Instagram Question Box`** (image) and **`Instagram Question Box – Video`** — render a two-panel sticker that replicates the Instagram "Ask me a question" story format.
|
||||
|
||||
**Layout**
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ Ask me a question ← header │ ← header_height px, header_color_hex
|
||||
├──────────────────────────────────┤
|
||||
│ │
|
||||
│ Your answer text here │ ← body_height px, body_color_hex
|
||||
│ │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
* All four outer corners are fully rounded (`corner_radius`) with **anti-aliased edges** (4× supersampled rendering)
|
||||
* The header/body divider is flat — only the outer corners are rounded
|
||||
* Optional soft drop-shadow (`box_shadow_enable`, `box_shadow_blur`, `box_shadow_distance`)
|
||||
|
||||
**Typography**
|
||||
|
||||
* Separate font, size, letter spacing, and fill color/alpha for **question** (header) and **answer** (body)
|
||||
* `answer_line_spacing` controls vertical gap between wrapped lines
|
||||
* `answer_padding` sets inner padding on all sides of the body text area
|
||||
* Answer text supports the same **HTML-like rich text** as the main overlay nodes (`<b>`, `<i>`, `<br>`, `<span color="…">`, `\n`) — bold/italic load the correct font variant automatically
|
||||
|
||||
**Positioning & Animation**
|
||||
|
||||
* `box_width`, `header_height`, `body_height` — independent pixel dimensions
|
||||
* `horizontal_alignment` / `vertical_alignment` + `x_shift` / `y_shift` for placement
|
||||
* Same full animation system as the main nodes (`fade_in`, `move_from_*`, easing, `pause_frames_before_start`)
|
||||
|
||||
**Video variant extras**
|
||||
|
||||
* `pause_seconds_before_start` (converted to frames using source FPS)
|
||||
* Audio preserved via ffmpeg re-mux
|
||||
* Optional `delete_original`
|
||||
|
||||
**Default values** are tuned for a 1080-wide portrait story:
|
||||
|
||||
| Parameter | Default |
|
||||
| --------- | ------- |
|
||||
| `box_width` | 950 |
|
||||
| `header_height` | 90 |
|
||||
| `body_height` | 200 |
|
||||
| `corner_radius` | 32 |
|
||||
| `question_font` | Arial Black |
|
||||
| `question_font_size` | 30 |
|
||||
| `answer_font_size` | 50 |
|
||||
| `box_shadow_blur` | 15 |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Installation
|
||||
## 📥 Installation
|
||||
|
||||
To install the **Advanced Text Overlay** node:
|
||||
Clone the repository inside your ComfyUI `custom_nodes` directory:
|
||||
|
||||
1. Locate your ComfyUI `custom_nodes` directory.
|
||||
Typically found at:
|
||||
```bash
|
||||
git clone https://github.com/scofano/ComfyUI-Advanced-TextOverlay
|
||||
```
|
||||
|
||||
```
|
||||
ComfyUI/custom_nodes/
|
||||
```
|
||||
2. Clone this repository into that directory:
|
||||
Install the required dependencies:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Restart ComfyUI — the nodes will appear under **Advanced Text Overlay**.
|
||||
|
||||
## 🎨 Font Selection & Name Resolution
|
||||
|
||||
The node includes a robust font management system that automatically populates with fonts installed on your system:
|
||||
|
||||
- **Windows**: Scans `C:\Windows\Fonts\`
|
||||
- **macOS**: Scans `/System/Library/Fonts/`, `/Library/Fonts/`, and `~/Library/Fonts/`
|
||||
- **Linux**: Scans `/usr/share/fonts/`, `/usr/local/share/fonts/`, `~/.fonts/`, and `~/.local/share/fonts/`
|
||||
|
||||
### True Font Name Resolution
|
||||
|
||||
Unlike many other nodes that use the font's filename (e.g., `califb.ttf` → "califb"), this module uses the **True Font Family Name** (e.g., "Californian FB").
|
||||
|
||||
- **Why this matters**: Rendering engines often fail to find fonts by filename, causing them to fall back to **Arial**. By resolving the internal metadata name, this module ensures your selected font is rendered correctly every time.
|
||||
- **Performance Caching**: Scanning hundreds of system fonts can be slow. This module automatically creates a `font_cache.json` file. The first scan takes a few seconds, but subsequent loads are near-instant (<1ms).
|
||||
|
||||
### Using the Font Dropdown
|
||||
|
||||
1. Select your desired font from the dropdown menu.
|
||||
2. The node will automatically resolve the "True Name" to the correct file path.
|
||||
3. Text will be rendered using the selected font, avoiding the "Arial fallback" bug.
|
||||
4. Works with both the image and video overlay nodes.
|
||||
|
||||
### For Developers
|
||||
|
||||
The font system is modular and can be reused in other ComfyUI nodes:
|
||||
|
||||
```python
|
||||
from .font_utils import get_available_fonts, get_font_path
|
||||
|
||||
class MyTextNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
fonts = get_available_fonts()
|
||||
return {
|
||||
"required": {
|
||||
"font": (fonts, {"default": fonts[0] if fonts else "Arial"}),
|
||||
# ... other inputs
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This provides a consistent font selection experience across all your nodes.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/scofano/ComfyUI-Advanced-TextOverlay
|
||||
```
|
||||
3. Restart ComfyUI.
|
||||
The node will appear under the **`image/text`** category as **“Advanced Text Overlay”**.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
1. Add the **Advanced Text Overlay** node to your ComfyUI workflow.
|
||||
2. Connect your image source to the node’s **image input**.
|
||||
3. Configure text and style parameters:
|
||||
### **Image Node: `Advanced Text Overlay`**
|
||||
|
||||
* Text content
|
||||
* Font and size
|
||||
* Fill and stroke colors
|
||||
* Alignment, spacing, and offset adjustments
|
||||
4. Connect the node’s output to your desired destination (e.g., preview, save image).
|
||||
Connect an image or batch → configure text parameters → render.
|
||||
|
||||
You can enter either plain text:
|
||||
|
||||
```text
|
||||
Hello world
|
||||
Second line
|
||||
```
|
||||
|
||||
or rich text:
|
||||
|
||||
```html
|
||||
Hello <b>world</b><br><span color="#00d0ff">Second line</span>
|
||||
```
|
||||
|
||||
You can also override line alignment for just part of the content:
|
||||
|
||||
```html
|
||||
<span align="center">Centered line</span>
|
||||
```
|
||||
|
||||
### **Video Node: `Advanced Text Overlay – Video`**
|
||||
|
||||
Provide a path to a video → configure overlay → output is written to ComfyUI's output directory.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Input Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------------------ | ------------- | ----------------------------------------------------------------------------- |
|
||||
| **image** | Image / Batch | Input image(s) to overlay text onto. |
|
||||
| **text** | String | The text content to render on the image. Supports multi-line text. |
|
||||
| **font** | String | Font name or filename (e.g., `arial.ttf`). Looks in `/fonts` or system fonts. |
|
||||
| **font_size** | Integer | Size of the text in pixels. |
|
||||
| **fill_color_hex** | String | Text fill color in hex format (e.g., `#FFFFFF`). |
|
||||
| **stroke_color_hex** | String | Stroke (outline) color in hex format (e.g., `#000000`). |
|
||||
| **stroke_thickness** | Float | Thickness of the text outline. |
|
||||
| **padding** | Float | Additional space around the text block. Useful for multi-line layouts. |
|
||||
| **horizontal_alignment** | Enum | `left`, `center`, or `right` horizontal alignment. |
|
||||
| **vertical_alignment** | Enum | `top`, `middle`, or `bottom` vertical alignment. |
|
||||
| **x_shift** | Float | Horizontal offset adjustment (pixels). |
|
||||
| **y_shift** | Float | Vertical offset adjustment (pixels). |
|
||||
| **line_spacing** | Float | Vertical spacing between lines of text. |
|
||||
| **letter_spacing** | Float | Horizontal spacing between letters. |
|
||||
### Text & Typography
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------- | ----------------------------------------- |
|
||||
| `text` | Text to draw; supports multiline `\n` and inline HTML-like tags such as `<b>`, `<i>`, `<br>`, and `<span color/bg>` |
|
||||
| `all_caps` | Force uppercase |
|
||||
| `font` | Font name or file; auto-searches `/fonts` |
|
||||
| `font_size` | Pixel size |
|
||||
| `letter_spacing` | Per-character spacing |
|
||||
| `line_spacing` | Spacing between lines |
|
||||
|
||||
### Rich Text Reference
|
||||
|
||||
| Syntax | Result |
|
||||
| ------ | ------ |
|
||||
| `<b>Text</b>` | Bold text when a matching font variant is available |
|
||||
| `<i>Text</i>` | Italic text when a matching font variant is available |
|
||||
| `<br>` | Line break |
|
||||
| `<span color="#RRGGBB">Text</span>` | Changes text color for that segment |
|
||||
| `<span bg="#RRGGBB">Text</span>` | Draws a background highlight behind that segment |
|
||||
| `<span align="center">Text</span>` | Centers the wrapped portion regardless of the node's `font_alignment` |
|
||||
| `<div align="right">Text</div>` | Right-aligns the wrapped portion regardless of the node's `font_alignment` |
|
||||
| `<span style="color:#fff; background-color:#000">Text</span>` | Combined inline styling |
|
||||
|
||||
### Rich Text Examples
|
||||
|
||||
```html
|
||||
Normal <b>bold</b> <i>italic</i>
|
||||
```
|
||||
|
||||
```html
|
||||
<span color="#FFD700">Gold</span> <span color="#00FFFF">Cyan</span>
|
||||
```
|
||||
|
||||
```html
|
||||
<span bg="#8B0000">Red highlight</span> and <span color="#FFFFFF" bg="#004488">white on blue</span>
|
||||
```
|
||||
|
||||
```html
|
||||
<span color="#FFD700"><b>Golden title</b></span><br>
|
||||
<span style="color:#ffffff; background-color:#333333">subtitle</span>
|
||||
```
|
||||
|
||||
```html
|
||||
Top line<br>
|
||||
<span align="center" color="#ffffff" bg="#444444"><b>Centered mid title</b></span><br>
|
||||
Bottom line
|
||||
```
|
||||
|
||||
### Rich Text Limitations
|
||||
|
||||
* This is **not** a full browser engine and does not support general HTML layout or arbitrary CSS.
|
||||
* Supported styling is intentionally limited to inline emphasis, line breaks, text color, and segment background color.
|
||||
* Per-segment backgrounds wrap naturally with the text layout; if text wraps, the highlight is drawn for each wrapped fragment.
|
||||
* Alignment overrides operate at the wrapped line/block level. If alignment changes mid-text, the renderer starts a new aligned line block for that portion.
|
||||
* Whole-block background settings (`bg_enable`, `bg_padding`, `bg_color_hex`, etc.) still apply independently from inline segment highlights.
|
||||
|
||||
### Color & Stroke
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------------ | ---------------------------------------- |
|
||||
| `fill_color_hex` | Hex fill color |
|
||||
| `fill_alpha` | Opacity 0–1 |
|
||||
| `stroke_enable` | Toggle outline |
|
||||
| `stroke_color_hex` | Hex outline color |
|
||||
| `stroke_alpha` | Outline opacity |
|
||||
| `stroke_thickness` | Relative thickness (scaled by font size) |
|
||||
|
||||
### Layout & Position
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------------- | --------------------------- |
|
||||
| `padding` | Padding around text block |
|
||||
| `horizontal_alignment` | `left` / `center` / `right` |
|
||||
| `vertical_alignment` | `top` / `middle` / `bottom` |
|
||||
| `x_shift`, `y_shift` | Pixel offsets |
|
||||
|
||||
### Background Box
|
||||
|
||||
| Parameter | Description |
|
||||
| -------------- | ------------------------- |
|
||||
| `bg_enable` | Toggle background box |
|
||||
| `bg_padding` | Padding around text block |
|
||||
| `bg_radius` | Rounded corner radius |
|
||||
| `bg_color_hex` | Hex color |
|
||||
| `bg_alpha` | Opacity |
|
||||
|
||||
### Shadow
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------------ | ------------- |
|
||||
| `shadow_enable` | Toggle shadow |
|
||||
| `shadow_distance` | Pixel offset |
|
||||
| `shadow_color_hex` | Shadow color |
|
||||
| `shadow_alpha` | Opacity |
|
||||
|
||||
### Animation
|
||||
|
||||
| Parameter | Description |
|
||||
| --------------------------- | ------------------------------- |
|
||||
| `animate` | Enable animation |
|
||||
| `animation_kind` | Fade or movement animation |
|
||||
| `animation_frames` | How many frames animation lasts |
|
||||
| `animation_opacity_target` | Target alpha |
|
||||
| `animation_ease` | Easing curve |
|
||||
| `pause_frames_before_start` | Delay before animation begins |
|
||||
|
||||
### Video-Specific Parameters
|
||||
|
||||
| Parameter | Description |
|
||||
| ---------------------------- | ------------------------------------ |
|
||||
| `video_path` | Input video |
|
||||
| `filename_prefix` | Naming prefix for output |
|
||||
| `delete_original` | Remove input after processing |
|
||||
| `pause_seconds_before_start` | Pause duration (converted to frames) |
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Output
|
||||
## 🧪 Output
|
||||
|
||||
The node outputs the modified image (or batch of images) with the specified text overlay applied according to the given configuration.
|
||||
* Image node → modified image/batch
|
||||
* Video node → MP4 written to ComfyUI output directory, with audio preserved
|
||||
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 629 KiB |
@@ -0,0 +1,48 @@
|
||||
from typing import Tuple
|
||||
|
||||
# Easing -------------------------------------------------------------
|
||||
def progress(t: int, T_minus1: int, ease: str) -> float:
|
||||
if T_minus1 <= 0:
|
||||
return 1.0
|
||||
x = float(t) / float(T_minus1)
|
||||
if ease == "linear":
|
||||
return x
|
||||
if ease == "ease_in":
|
||||
return x * x
|
||||
if ease == "ease_out":
|
||||
return 1 - (1 - x) * (1 - x)
|
||||
if ease == "ease_in_out":
|
||||
if x < 0.5:
|
||||
return 2 * x * x
|
||||
return 1 - (-2 * x + 2) ** 2 / 2
|
||||
return x
|
||||
|
||||
# Opacity ------------------------------------------------------------
|
||||
def compute_opacity(kind: str, p: float, target: float) -> float:
|
||||
"""
|
||||
kind: 'fade_in' | 'fade_out' | others
|
||||
p in [0,1], target in [0,1]
|
||||
"""
|
||||
target = max(0.0, min(1.0, float(target)))
|
||||
if kind == "fade_out":
|
||||
return target * (1.0 - p)
|
||||
if kind == "fade_in":
|
||||
return target * p
|
||||
return target
|
||||
|
||||
# Offsets ------------------------------------------------------------
|
||||
def compute_offsets(kind: str, p: float, img_w: int, img_h: int) -> Tuple[int, int]:
|
||||
"""
|
||||
Move from off-screen-ish towards the final position.
|
||||
p=0 => max offset, p=1 => 0 offset.
|
||||
"""
|
||||
dx = dy = 0.0
|
||||
if kind == "move_from_left":
|
||||
dx = -(img_w * (1.0 - p) * 0.5)
|
||||
elif kind == "move_from_right":
|
||||
dx = (img_w * (1.0 - p) * 0.5)
|
||||
elif kind == "move_from_top":
|
||||
dy = -(img_h * (1.0 - p) * 0.5)
|
||||
elif kind == "move_from_bottom":
|
||||
dy = (img_h * (1.0 - p) * 0.5)
|
||||
return int(round(dx)), int(round(dy))
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
+291
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
Font utilities for ComfyUI nodes.
|
||||
Provides cross-platform font discovery and management functionality.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import glob
|
||||
import re
|
||||
import json
|
||||
from typing import List, Dict, Optional
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from fontTools import ttLib
|
||||
except ImportError:
|
||||
ttLib = None
|
||||
|
||||
CACHE_FILE = Path(__file__).parent / "font_cache.json"
|
||||
|
||||
class FontManager:
|
||||
"""Centralized font discovery and management for ComfyUI nodes."""
|
||||
|
||||
def __init__(self):
|
||||
self._name_to_path: Dict[str, str] = {}
|
||||
self._scanned = False
|
||||
self._load_cache()
|
||||
|
||||
def _load_cache(self) -> None:
|
||||
"""Load font name mapping from JSON cache if it exists."""
|
||||
if CACHE_FILE.exists():
|
||||
try:
|
||||
with open(CACHE_FILE, "r", encoding="utf-8") as f:
|
||||
self._name_to_path = json.load(f)
|
||||
if self._name_to_path:
|
||||
self._scanned = True
|
||||
except Exception as e:
|
||||
print(f"Error loading font cache: {e}")
|
||||
|
||||
def _save_cache(self) -> None:
|
||||
"""Save current font mapping to JSON cache."""
|
||||
try:
|
||||
with open(CACHE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self._name_to_path, f, indent=4)
|
||||
except Exception as e:
|
||||
print(f"Error saving font cache: {e}")
|
||||
|
||||
def _get_font_family_name(self, font_path: str) -> Optional[str]:
|
||||
"""Extract the Font Family name using fontTools."""
|
||||
if ttLib is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Use lazy=True to avoid loading the entire font into memory
|
||||
# For .ttc files, we just look at the first font (fontNumber=0)
|
||||
font = ttLib.TTFont(font_path, fontNumber=0, lazy=True)
|
||||
|
||||
name_record = None
|
||||
if 'name' in font:
|
||||
for record in font['name'].names:
|
||||
# Name ID 1 = Font Family
|
||||
if record.nameID == 1:
|
||||
# Priority: Windows Platform (3) + US English (1033)
|
||||
if record.platformID == 3 and record.langID == 1033:
|
||||
name_record = record
|
||||
break
|
||||
# Fallback to any English or the first record found
|
||||
elif record.langID == 1033 or name_record is None:
|
||||
name_record = record
|
||||
|
||||
if name_record:
|
||||
return name_record.toUnicode()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_available_fonts(self) -> List[str]:
|
||||
"""
|
||||
Returns a sorted list of available font names for dropdown menus.
|
||||
Caches results for performance.
|
||||
"""
|
||||
if not self._scanned:
|
||||
self._scan_system_fonts()
|
||||
|
||||
return sorted(self._name_to_path.keys())
|
||||
|
||||
def get_font_path(self, font_name: str) -> str:
|
||||
"""
|
||||
Converts a font name to its actual file path.
|
||||
Returns the original name if not found in our cache (backward compatibility).
|
||||
"""
|
||||
if not self._scanned:
|
||||
self._scan_system_fonts()
|
||||
|
||||
return self._name_to_path.get(font_name, font_name)
|
||||
|
||||
def get_font_variant_path(self, font_name: str, bold: bool = False, italic: bool = False) -> str:
|
||||
"""
|
||||
Best-effort lookup for bold/italic variants of a selected font.
|
||||
|
||||
If a matching variant cannot be found, returns the regular font path.
|
||||
"""
|
||||
if not self._scanned:
|
||||
self._scan_system_fonts()
|
||||
|
||||
base_path = self.get_font_path(font_name)
|
||||
if not bold and not italic:
|
||||
return base_path
|
||||
|
||||
if bold and italic:
|
||||
return (
|
||||
self._find_variant_path(font_name, base_path, bold=True, italic=True)
|
||||
or self._find_variant_path(font_name, base_path, bold=True, italic=False)
|
||||
or self._find_variant_path(font_name, base_path, bold=False, italic=True)
|
||||
or base_path
|
||||
)
|
||||
|
||||
return self._find_variant_path(font_name, base_path, bold=bold, italic=italic) or base_path
|
||||
|
||||
def _normalize_font_name(self, value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "", (value or "").lower())
|
||||
|
||||
def _find_variant_path(self, font_name: str, base_path: str, bold: bool = False, italic: bool = False) -> Optional[str]:
|
||||
normalized_targets = []
|
||||
|
||||
if font_name:
|
||||
normalized_targets.append(self._normalize_font_name(font_name))
|
||||
|
||||
if base_path and os.path.exists(base_path):
|
||||
base_stem = Path(base_path).stem
|
||||
normalized_targets.append(self._normalize_font_name(base_stem))
|
||||
|
||||
normalized_targets = [t for t in dict.fromkeys(normalized_targets) if t]
|
||||
if not normalized_targets:
|
||||
return None
|
||||
|
||||
bold_markers = ["bold", "semibold", "demibold", "extrabold", "black", "heavy"]
|
||||
italic_markers = ["italic", "oblique", "slanted", "kursiv"]
|
||||
|
||||
best_match = None
|
||||
best_score = None
|
||||
|
||||
for candidate_name, candidate_path in self._name_to_path.items():
|
||||
normalized_name = self._normalize_font_name(candidate_name)
|
||||
if not normalized_name:
|
||||
continue
|
||||
|
||||
if not any(target in normalized_name or normalized_name in target for target in normalized_targets):
|
||||
continue
|
||||
|
||||
if bold and not any(marker in normalized_name for marker in bold_markers):
|
||||
continue
|
||||
if italic and not any(marker in normalized_name for marker in italic_markers):
|
||||
continue
|
||||
|
||||
score = len(normalized_name)
|
||||
if bold and "bold" in normalized_name:
|
||||
score -= 10
|
||||
if italic and "italic" in normalized_name:
|
||||
score -= 10
|
||||
for target in normalized_targets:
|
||||
if normalized_name == target:
|
||||
score -= 2
|
||||
elif normalized_name.startswith(target) or target.startswith(normalized_name):
|
||||
score -= 1
|
||||
|
||||
if best_score is None or score < best_score:
|
||||
best_score = score
|
||||
best_match = candidate_path
|
||||
|
||||
return best_match
|
||||
|
||||
def _scan_system_fonts(self) -> None:
|
||||
"""Platform-specific font discovery."""
|
||||
if self._scanned and self._name_to_path:
|
||||
return
|
||||
|
||||
self._name_to_path = {}
|
||||
|
||||
if sys.platform == "win32":
|
||||
self._scan_windows_fonts()
|
||||
elif sys.platform == "darwin": # macOS
|
||||
self._scan_macos_fonts()
|
||||
else: # Linux and other Unix-like systems
|
||||
self._scan_linux_fonts()
|
||||
|
||||
# Add some common fallback fonts
|
||||
self._add_fallback_fonts()
|
||||
|
||||
# Save results to cache for next time
|
||||
self._save_cache()
|
||||
self._scanned = True
|
||||
|
||||
def _scan_windows_fonts(self) -> None:
|
||||
"""Scan Windows system fonts directory."""
|
||||
try:
|
||||
fonts_dir = Path(os.environ.get("WINDIR", "C:\\Windows")) / "Fonts"
|
||||
if fonts_dir.exists():
|
||||
self._scan_directory(fonts_dir)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _scan_macos_fonts(self) -> None:
|
||||
"""Scan macOS system fonts directories."""
|
||||
font_dirs = [
|
||||
Path("/System/Library/Fonts"),
|
||||
Path("/Library/Fonts"),
|
||||
Path.home() / "Library/Fonts"
|
||||
]
|
||||
|
||||
for fonts_dir in font_dirs:
|
||||
if fonts_dir.exists():
|
||||
self._scan_directory(fonts_dir)
|
||||
|
||||
def _scan_linux_fonts(self) -> None:
|
||||
"""Scan Linux system fonts directories."""
|
||||
font_dirs = [
|
||||
Path("/usr/share/fonts"),
|
||||
Path("/usr/local/share/fonts"),
|
||||
Path.home() / ".fonts",
|
||||
Path.home() / ".local/share/fonts"
|
||||
]
|
||||
|
||||
for fonts_dir in font_dirs:
|
||||
if fonts_dir.exists():
|
||||
self._scan_directory(fonts_dir)
|
||||
|
||||
def _scan_directory(self, fonts_dir: Path) -> None:
|
||||
"""Scan a directory for font files."""
|
||||
patterns = ["*.ttf", "*.otf", "*.ttc", "*.woff", "*.woff2"]
|
||||
|
||||
for pattern in patterns:
|
||||
for font_file in fonts_dir.glob(f"**/{pattern}"):
|
||||
try:
|
||||
# Try to get the internal font family name
|
||||
font_path = str(font_file)
|
||||
font_name = self._get_font_family_name(font_path)
|
||||
|
||||
# Fallback to filename stem if metadata extraction fails
|
||||
if not font_name:
|
||||
font_name = font_file.stem
|
||||
|
||||
# Only add if we don't already have this name (avoid duplicates)
|
||||
if font_name not in self._name_to_path:
|
||||
self._name_to_path[font_name] = font_path
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def _add_fallback_fonts(self) -> None:
|
||||
"""Add common fallback fonts that might not be detected."""
|
||||
fallbacks = {
|
||||
"Arial": "Arial",
|
||||
"Times New Roman": "Times New Roman",
|
||||
"Courier New": "Courier New",
|
||||
"Helvetica": "Helvetica",
|
||||
"Verdana": "Verdana",
|
||||
"Georgia": "Georgia",
|
||||
"Trebuchet MS": "Trebuchet MS",
|
||||
"Impact": "Impact",
|
||||
"Comic Sans MS": "Comic Sans MS",
|
||||
"Arial Black": "Arial Black",
|
||||
"Tahoma": "Tahoma",
|
||||
"Lucida Sans Unicode": "Lucida Sans Unicode",
|
||||
"MS Sans Serif": "MS Sans Serif",
|
||||
"MS Serif": "MS Serif",
|
||||
"Palatino Linotype": "Palatino Linotype",
|
||||
"Arial Narrow": "Arial Narrow"
|
||||
}
|
||||
|
||||
for name, path in fallbacks.items():
|
||||
if name not in self._name_to_path:
|
||||
self._name_to_path[name] = path
|
||||
|
||||
|
||||
# Global instance for easy reuse across nodes
|
||||
font_manager = FontManager()
|
||||
|
||||
|
||||
def get_available_fonts() -> List[str]:
|
||||
"""Convenience function to get available fonts."""
|
||||
return font_manager.get_available_fonts()
|
||||
|
||||
|
||||
def get_font_path(font_name: str) -> str:
|
||||
"""Convenience function to get font path from name."""
|
||||
return font_manager.get_font_path(font_name)
|
||||
|
||||
|
||||
def get_font_variant_path(font_name: str, bold: bool = False, italic: bool = False) -> str:
|
||||
"""Convenience function to get a bold/italic font variant path if available."""
|
||||
return font_manager.get_font_variant_path(font_name, bold=bold, italic=italic)
|
||||
+20
-6
@@ -1,13 +1,27 @@
|
||||
# pyproject.toml
|
||||
[project]
|
||||
name = "advanced-textoverlay" # Unique and immutable node identifier.
|
||||
description = "Advanced Text Overlay Node for ComfyUI featuring rebuilt rendering logic, precise alignment, custom fonts, and extended styling controls."
|
||||
version = "1.1.0" # Semantic versioning
|
||||
dependencies = [] # Populated from requirements.txt if present.
|
||||
name = "advanced-textoverlay"
|
||||
description = "4 nodes: Advanced Text Overlay (image + video) with rich HTML text, animations, and full typography controls; Instagram Question Box (image + video) with two-panel sticker layout, anti-aliased corners, independent header/body heights, and soft drop-shadow."
|
||||
version = "1.5.0"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"torch",
|
||||
"Pillow",
|
||||
"imageio",
|
||||
"tqdm",
|
||||
]
|
||||
classifiers = [
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/scofano/ComfyUI-Advanced-TextOverlay"
|
||||
Documentation = "https://github.com/scofano/ComfyUI-Advanced-TextOverlay#readme"
|
||||
Issues = "https://github.com/scofano/ComfyUI-Advanced-TextOverlay/issues"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "dansco"
|
||||
DisplayName = "Advanced Text Overlay"
|
||||
DisplayName = "Advanced Text Overlay"
|
||||
Icon = "https://raw.githubusercontent.com/scofano/ComfyUI-Advanced-TextOverlay/refs/heads/main/ato-icon.jpg"
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
numpy
|
||||
torch
|
||||
Pillow
|
||||
Pillow
|
||||
imageio
|
||||
tqdm
|
||||
fonttools
|
||||
Reference in New Issue
Block a user