Add Playwright rendering with auto-sizing, padding controls & transparency fix

This commit is contained in:
ru4ls
2025-10-22 23:14:51 +07:00
parent 17b9c45f2d
commit 94317cbc4d
9 changed files with 272 additions and 58 deletions
+37
View File
@@ -0,0 +1,37 @@
# Changelog
All notable changes to the Google Font Text Image Node for ComfyUI will be documented in this file.
## [2025-10-22]
### Added
- **Playwright Integration**: Migrated from html2image to Playwright for more reliable rendering
- **Automatic Dimension Calculation**: Added ability to set width or height to -1 for automatic sizing based on text content
- **Text Wrapping**: Implemented proper text wrapping when using fixed canvas dimensions
- **Individual Padding Controls**: Added separate controls for top, right, bottom, and left padding (0-200px range)
- **Enhanced Text Positioning**: Improved CSS layout with precise control over text positioning
### Changed
- Replaced `html2image` dependency with `playwright` for better rendering capabilities
- Updated HTML/CSS generation to support both auto-sizing and fixed-dimension layouts
- Modified width/height parameters to accept -1 for automatic sizing
- Improved font loading with explicit wait time for better reliability
- Enhanced dimension calculation logic to account for padding values in auto-sizing mode
### Fixed
- Text cutoff issues that were common with html2image
- Inability to automatically size images based on content
- Poor text wrapping behavior with long text strings
- Limited control over text positioning within the canvas
### Deprecated
- html2image rendering engine (replaced by Playwright)
## [2024-XX-XX] - Initial Release
### Added
- Initial release of the Google Font Text Image Node
- Support for Google Fonts API integration
- Basic text rendering with typographic controls
- Dual output modes (Custom Text and Standard Character Set)
- Transparent background support
+39 -13
View File
@@ -7,13 +7,13 @@
This custom node provides a powerful and flexible way to create images from text with advanced typographic controls. It's an ideal tool for generating inputs for Typography, creating titles and watermarks, or producing font specimens for multimodal models to analyze and generate.
![Preview](media/preview.png)
![Preview](media/example-preview.png)
Click below to see the video example:
<br/>
<p align="center">
<a href="https://www.youtube.com/watch?v=KA2Uxha0GsQ" target="_blank">
<img src="https://github.com/ru4ls/ComfyUI_Google-Fonts/blob/main/media/example-1.png?raw=true" alt="Watch the Video Demo " width="90%">
<img src="https://github.com/ru4ls/ComfyUI_Google-Fonts/blob/main/media/example-1.png?raw=true" alt="Watch the Video Demo" width="90%">
</a>
</p>
@@ -26,6 +26,9 @@ Click below to see the video example:
- **Dual Output Modes:**
- **Custom Text:** Render any text you input, respecting line breaks (`\n`).
- **Standard Character Set:** Generate a font specimen image (A-Z, a-z, 0-9, symbols) to capture the font's overall style as reference for multimodal models.
- **Automatic Dimension Calculation:** Set width or height to -1 to automatically size the image based on text content (requires Playwright).
- **Text Wrapping:** Text automatically wraps within specified dimensions when using fixed width/height values.
- **Individual Padding Controls:** Fine-tune text positioning with independent controls for top, right, bottom, and left padding.
- **Transparent Background:** Output a clean RGB image and a corresponding alpha mask, perfect for compositing and layering.
- **Dynamic Font Variant Validation:** The node intelligently checks if a selected font weight/style is available and provides a graceful fallback if it isn't.
@@ -47,9 +50,15 @@ Click below to see the video example:
```bash
pip install -r ComfyUI_Google-Font/requirements.txt
```
*(This will install `requests`, `html2image`, and `python-dotenv`.)*
4. **Restart ComfyUI:**
4. **Install Playwright browser binaries:**
Run the following command to install the necessary browser binaries for Playwright:
```bash
playwright install
```
*(Note: You may need to run this in your activated virtual environment)*
5. **Restart ComfyUI:**
You must completely restart ComfyUI for the node to be loaded.
## Google Fonts API Key (Recommended)
@@ -80,8 +89,9 @@ The node will automatically detect and use this key on the next restart.
| `font_family` | A dropdown list of all available Google Fonts. |
| `output_mode` | - **Custom Text:** Renders the text from the `text` input box.<br>- **Standard Character Set:** Renders a predefined set of characters. |
| `text` | The text you want to display. Supports multiple lines. Ignored if `output_mode` is "Standard Character Set". |
| `width` / `height` | The dimensions of the output image in pixels. |
| `font_size` | The size of the font in points. **(See Troubleshooting for text cut-off)** |
| `dimension_mode` | Choose between "Auto" (automatic sizing based on text content) or "Define Manually" (fixed width/height). |
| `width` / `height` | The dimensions of the output image in pixels (64-8192). Only used when `dimension_mode` is "Define Manually". |
| `font_size` | The size of the font in points. |
| `font_weight` | The thickness of the font (e.g., `100` for Thin, `400`/`regular` for Normal, `700` for Bold). |
| `font_style` | Choose between `normal` and `italic`. |
| `text_align` | Horizontal alignment of the text: `center`, `left`, or `right`. |
@@ -90,6 +100,10 @@ The node will automatically detect and use this key on the next restart.
| `text_color` | The color of the text in hex format (e.g., `#000000` for black). Use with an external color picker node. |
| `background_color` | The color of the background in hex format (e.g., `#FFFFFF` for white). Ignored if `transparent_background` is checked. |
| `transparent_background` | **(Recommended)** If checked, the background will be transparent, and the `MASK` output will contain the text's shape. |
| `padding_top` | Top padding in pixels (0-200). Controls the space between text and the top edge. |
| `padding_right` | Right padding in pixels (0-200). Controls the space between text and the right edge. |
| `padding_bottom` | Bottom padding in pixels (0-200). Controls the space between text and the bottom edge. |
| `padding_left` | Left padding in pixels (0-200). Controls the space between text and the left edge. |
### Node Outputs
@@ -107,18 +121,26 @@ A powerful use case is to layer the generated text over another image.
3. Load a base image using a `Load Image` node.
4. Use a `Composite` or `Layer Style` node to combine the base image with the `IMAGE` and `MASK` from the Google Font Node.
![Example](media/example.png)
## Troubleshooting
#### **Help! My text is getting cut off (truncated)!**
This is the most common issue. This node **does not automatically resize the font** to fit the image. If the text is too large for the canvas, it will be cut off.
This issue has been significantly improved with the Playwright migration. You now have two solutions:
To fix this, you must **manually adjust the settings**:
**Solution 1: Use Auto-Sizing**
1. Set `dimension_mode` to **Auto** for automatic sizing based on text content.
**Solution 2: Manual Adjustments**
1. **Decrease** the `font_size`.
2. **Increase** the `width` or `height` of the image.
3. **Decrease** the `line_height` if you have multiple lines of text.
2. **Increase** the `width` or `height` of the image when in "Define Manually" mode.
3. **Increase** the `padding` values to provide more space around the text.
4. **Decrease** the `line_height` if you have multiple lines of text.
#### **Playwright Browser Not Found Error**
If you get a browser not found error after installation:
1. Run `playwright install` in your terminal/command prompt to install browser binaries.
2. Make sure to activate your virtual environment before running the command.
#### **The node doesn't appear in ComfyUI after installation.**
@@ -133,4 +155,8 @@ This can happen if the node fails to connect to the Google Fonts API.
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Changelog
See the [CHANGELOG](CHANGELOG.md) file for a complete history of changes.
+95 -27
View File
@@ -26,8 +26,9 @@ def get_google_fonts_data(api_key):
print(f"GoogleFontNode: ERROR! Failed to fetch Google Fonts list: {e}")
return []
# --- SIMPLE GOOGLE FONT HTML RENDERER ---
def create_simple_google_font_html(font_family, text, width, height, style_params):
# --- PLAYWRIGHT-READY HTML RENDERER ---
def create_playwright_google_font_html(font_family, text, style_params, width=None, height=None):
"""Creates HTML content optimized for Playwright with automatic sizing and optional text wrapping"""
google_font_url_name = font_family.replace(' ', '+')
escaped_text = text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('\n', '<br>')
@@ -39,33 +40,100 @@ def create_simple_google_font_html(font_family, text, width, height, style_param
text_transform = style_params.get('text_transform', 'none')
text_color = style_params.get('text_color', '#000000')
background_color = style_params.get('background_color', '#FFFFFF')
padding_top = style_params.get('padding_top', 20)
padding_right = style_params.get('padding_right', 20)
padding_bottom = style_params.get('padding_bottom', 20)
padding_left = style_params.get('padding_left', 20)
# Construct the font URL with the specific weights and styles needed
font_request_url = f"https://fonts.googleapis.com/css2?family={google_font_url_name}:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
html_content = f"""
<!DOCTYPE html><html><head><meta charset="UTF-8">
<link href="{font_request_url}" rel="stylesheet">
<style>
html, body {{ height: 100%; width: 100%; margin: 0; padding: 0; box-sizing: border-box; background-color: {background_color}; }}
body {{
display: flex;
justify-content: {'center' if text_align == 'center' else 'flex-start' if text_align == 'left' else 'flex-end'};
align-items: center;
}}
.text-container {{
font-family: '{font_family}', sans-serif;
font-size: {font_size}px;
font-weight: {font_weight};
font-style: {font_style};
color: {text_color};
text-align: {text_align};
line-height: {line_height};
text-transform: {text_transform};
width: 100%;
padding: 20px;
box-sizing: border-box;
}}
</style></head><body><div class="text-container">{escaped_text}</div></body></html>
"""
# If specific width/height are provided, we want to enable text wrapping
if width and width > 0:
# Fixed width layout with text wrapping
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="{font_request_url}" rel="stylesheet">
<style>
html, body {{
margin: 0;
padding: 0;
box-sizing: border-box;
background-color: {background_color};
font-family: '{font_family}', sans-serif;
width: {width}px;
height: {height if height and height > 0 else 'auto'}px;
}}
.text-container {{
font-size: {font_size}px;
font-weight: {font_weight};
font-style: {font_style};
color: {text_color};
text-align: {text_align};
line-height: {line_height};
text-transform: {text_transform};
width: calc(100% - {padding_left + padding_right}px);
height: calc(100% - {padding_top + padding_bottom}px);
padding: {padding_top}px {padding_right}px {padding_bottom}px {padding_left}px;
word-wrap: break-word;
overflow: hidden;
display: flex;
align-items: center;
justify-content: {'center' if text_align == 'center' else 'flex-start' if text_align == 'left' else 'flex-end'};
}}
</style>
</head>
<body>
<div class="text-container">{escaped_text}</div>
</body>
</html>
"""
else:
# Auto-sizing layout without fixed dimensions
html_content = f"""
<!DOCTYPE html>
<html style="width: auto; height: auto; overflow: visible;">
<head>
<meta charset="UTF-8">
<link href="{font_request_url}" rel="stylesheet">
<style>
html, body {{
margin: 0;
padding: 0;
width: auto !important;
height: auto !important;
min-width: 0;
min-height: 0;
overflow: visible;
box-sizing: border-box;
background-color: {background_color};
font-family: '{font_family}', sans-serif;
}}
.text-container {{
font-size: {font_size}px;
font-weight: {font_weight};
font-style: {font_style};
color: {text_color};
text-align: {text_align};
line-height: {line_height};
text-transform: {text_transform};
padding: {padding_top}px {padding_right}px {padding_bottom}px {padding_left}px;
display: inline-block;
max-width: none !important;
width: auto !important;
height: auto !important;
overflow: visible;
white-space: pre-wrap; /* Allow line breaks from \\n but no forced wrapping */
}}
</style>
</head>
<body>
<div class="text-container">{escaped_text}</div>
</body>
</html>
"""
return html_content
Binary file not shown.

Before

Width:  |  Height:  |  Size: 992 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

+100 -17
View File
@@ -2,12 +2,12 @@
import os, torch, numpy as np
from PIL import Image, ImageOps
from html2image import Html2Image
import folder_paths, time
from playwright.async_api import async_playwright
from .font_utils import (
load_api_key, get_google_fonts_data,
create_simple_google_font_html
create_playwright_google_font_html
)
# --- Load Data on Startup ---
@@ -27,18 +27,27 @@ class GoogleFontTextNode:
def INPUT_TYPES(s):
return {
"required": {
# Font selection and styling
"font_family": (FONT_FAMILIES,),
"output_mode": (["Custom Text", "Standard Character Set"],),
"text": ("STRING", {"multiline": True, "default": "Sample Text"}),
"width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"font_size": ("INT", {"default": 48, "min": 1, "max": 1024, "step": 1}),
"font_weight": (["100", "200", "300", "400", "500", "600", "700", "800", "900"], {"default": "400"}),
"font_style": (["normal", "italic"],),
# Text content and formatting
"output_mode": (["Custom Text", "Standard Character Set"],),
"text": ("STRING", {"multiline": True, "default": "Sample Text"}),
"text_align": (["center", "left", "right"],),
"line_height": ("FLOAT", {"default": 1.2, "min": 0.1, "max": 5.0, "step": 0.1}),
"text_transform": (["none", "uppercase", "lowercase", "capitalize"],),
"text_color": ("STRING", {"default": "#000000"}),
# Dimension mode and settings
"dimension_mode": (["Auto", "Define Manually"], {"default": "Auto"}),
"width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}),
"padding_top": ("INT", {"default": 20, "min": 0, "max": 200, "step": 1}),
"padding_right": ("INT", {"default": 20, "min": 0, "max": 200, "step": 1}),
"padding_bottom": ("INT", {"default": 20, "min": 0, "max": 200, "step": 1}),
"padding_left": ("INT", {"default": 20, "min": 0, "max": 200, "step": 1}),
# Background and transparency
"background_color": ("STRING", {"default": "#FFFFFF"}),
"transparent_background": ("BOOLEAN", {"default": True}),
}
@@ -48,13 +57,13 @@ class GoogleFontTextNode:
FUNCTION = "render_text"
CATEGORY = "Ru4ls/Google Fonts"
def render_text(self, font_family, output_mode, text, width, height, font_size, font_weight, font_style,
text_align, line_height, text_transform, text_color, background_color, transparent_background):
async def render_text(self, font_family, output_mode, text, dimension_mode, width, height, font_size, font_weight, font_style,
text_align, line_height, text_transform, text_color, background_color, transparent_background,
padding_top, padding_right, padding_bottom, padding_left):
# Prepare text based on output mode
render_text = text if output_mode == "Custom Text" else STANDARD_CHAR_SET
# Prepare style parameters
style_params = {
'font_size': font_size,
'font_weight': font_weight,
@@ -63,7 +72,11 @@ class GoogleFontTextNode:
'line_height': line_height,
'text_transform': text_transform,
'text_color': text_color,
'background_color': 'transparent' if transparent_background else background_color
'background_color': background_color if not transparent_background else 'transparent',
'padding_top': max(1, padding_top),
'padding_right': max(1, padding_right),
'padding_bottom': max(1, padding_bottom),
'padding_left': max(1, padding_left)
}
# Validate font variant
@@ -86,16 +99,72 @@ class GoogleFontTextNode:
style_params['font_style'] = font_style
style_params['font_weight'] = font_weight
# Render image using the backend engine
# Render image using Playwright
output_dir = folder_paths.get_temp_directory()
hti = Html2Image(output_path=output_dir)
html_str = create_simple_google_font_html(font_family, render_text, width, height, style_params)
# Determine dimensions based on mode
if dimension_mode == "Auto":
render_width = None
render_height = None
else:
render_width = width
render_height = height
# Create HTML content optimized for Playwright with width/height for text wrapping
html_str = create_playwright_google_font_html(font_family, render_text, style_params, render_width, render_height)
output_filename = f"google_font_{int(time.time())}.png"
hti.screenshot(html_str=html_str, save_as=output_filename, size=(width, height))
image_path = os.path.join(output_dir, output_filename)
# Use Playwright to render HTML to image
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# For auto-sizing mode, use a large initial viewport to prevent text wrapping before measurement
if dimension_mode == "Auto":
await page.set_viewport_size({"width": 8192, "height": 8192}) # Large viewport to prevent wrapping
# Set the HTML content
await page.set_content(html_str)
# Wait for fonts to load
await page.wait_for_timeout(1000) # Wait for font loading
# Get the bounding box of the text container to calculate actual dimensions
bbox = await page.locator('.text-container').bounding_box()
# Determine dimensions based on the selected mode
if dimension_mode == "Auto":
# Auto-size based on content
if bbox:
# Add individual padding values for auto-sizing (use internally adjusted values)
actual_padding_left = max(1, padding_left)
actual_padding_right = max(1, padding_right)
actual_padding_top = max(1, padding_top)
actual_padding_bottom = max(1, padding_bottom)
calculated_width = int(bbox['width']) + actual_padding_left + actual_padding_right
calculated_height = int(bbox['height']) + actual_padding_top + actual_padding_bottom
final_width = min(max(calculated_width, 64), 8192) # Keep within bounds
final_height = min(max(calculated_height, 64), 8192) # Keep within bounds
else:
# Fallback if bbox not available
final_width = 512
final_height = 512
else:
# Use user-defined dimensions
final_width = width
final_height = height
# Set viewport size to calculated dimensions
await page.set_viewport_size({"width": final_width, "height": final_height})
# Take screenshot - ensure transparent background when needed
await page.screenshot(path=image_path, type='png', omit_background=transparent_background)
await browser.close()
# Process the output image
i = Image.open(image_path)
i = ImageOps.exif_transpose(i)
@@ -106,13 +175,27 @@ class GoogleFontTextNode:
# Split into RGB image and alpha mask
image_rgb = Image.merge("RGB", i.split()[0:3])
mask = i.split()[3]
alpha_channel = i.split()[3] # Get the alpha channel (mask)
# Convert to ComfyUI tensors
image_tensor = np.array(image_rgb).astype(np.float32) / 255.0
mask_tensor = np.array(mask).astype(np.float32) / 255.0
mask_np = np.array(alpha_channel).astype(np.float32) / 255.0
return (torch.from_numpy(image_tensor)[None,], torch.from_numpy(mask_tensor).unsqueeze(0))
# Ensure mask is 2D (H, W) as expected by ComfyUI - some operations may add extra dims
if mask_np.ndim > 2:
# If it's 3D, squeeze the channel dimension to make it 2D
if mask_np.shape[-1] == 1:
mask_np = mask_np.squeeze(-1) # Remove singleton channel dimension
elif mask_np.shape[0] == 1:
mask_np = mask_np.squeeze(0) # Remove singleton batch dimension
else:
# If multiple channels somehow, take just the first one
mask_np = mask_np[..., 0] if mask_np.ndim == 3 else mask_np
# The mask should be (H, W) shape now, then unsqueeze to (1, H, W)
mask_tensor = torch.from_numpy(mask_np).unsqueeze(0)
return (torch.from_numpy(image_tensor)[None,], mask_tensor)
# ====================================================================================================
# NODE MAPPINGS
+1 -1
View File
@@ -1,3 +1,3 @@
requests
html2image
playwright
python-dotenv