diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..15a8f14 --- /dev/null +++ b/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index dbfe481..d2b2d00 100644 --- a/README.md +++ b/README.md @@ -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:

- Watch the Video Demo + Watch the Video Demo

@@ -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.
- **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. \ No newline at end of file +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. \ No newline at end of file diff --git a/font_utils.py b/font_utils.py index 40261b8..b6ce54a 100644 --- a/font_utils.py +++ b/font_utils.py @@ -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('&', '&').replace('<', '<').replace('>', '>').replace('\n', '
') @@ -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""" - - -
{escaped_text}
- """ + # 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""" + + + + + + + + +
{escaped_text}
+ + + """ + else: + # Auto-sizing layout without fixed dimensions + html_content = f""" + + + + + + + + +
{escaped_text}
+ + + """ + return html_content \ No newline at end of file diff --git a/media/example-1.png b/media/example-1.png deleted file mode 100644 index 7a7702b..0000000 Binary files a/media/example-1.png and /dev/null differ diff --git a/media/example-preview.png b/media/example-preview.png new file mode 100644 index 0000000..85b6a8d Binary files /dev/null and b/media/example-preview.png differ diff --git a/media/example.png b/media/example.png deleted file mode 100644 index 53d7fd1..0000000 Binary files a/media/example.png and /dev/null differ diff --git a/media/preview.png b/media/preview.png deleted file mode 100644 index ed09135..0000000 Binary files a/media/preview.png and /dev/null differ diff --git a/nodes.py b/nodes.py index 759b36c..81ac6d6 100644 --- a/nodes.py +++ b/nodes.py @@ -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 diff --git a/requirements.txt b/requirements.txt index dff5f34..a6fbe67 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ requests -html2image +playwright python-dotenv \ No newline at end of file