contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
976 lines (956 loc) • 42.6 kB
JavaScript
import { BaseTool } from './BaseTool.js';
import { BrowserServiceManager } from './BrowserService.js';
import { LLMFactory } from '../llm/LLMFactory.js';
import path from 'path';
import fs from 'fs/promises';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* TextToImageTool - Render text as images using HTML/CSS layouts and Playwright screenshots
*
* This tool provides a unified interface for converting text into images using:
* - Predefined HTML/CSS templates for common layouts
* - Custom HTML/CSS injection for advanced layouts
* - Playwright browser automation for high-quality screenshots
* - Tailwind CSS integration for modern styling
*/
export class TextToImageTool extends BaseTool {
constructor(baseDir) {
super();
this.baseDir = baseDir;
}
getName() {
return 'text_to_image';
}
getDescription() {
return 'Render text as high-quality images using AI-generated HTML layouts with ONLY Tailwind CSS classes and browser screenshots. Creates custom, responsive designs based on your requirements using pure Tailwind utilities - NO raw CSS or custom styles. SUPPORTS TRANSPARENT BACKGROUNDS: Generate PNG images with transparent backgrounds for overlays and compositing. SUPPORTS ITERATIVE DESIGN: Saves HTML files alongside images for editing and re-rendering. Agents can modify saved HTML files using file tools, then re-render instantly without AI generation. Perfect for creating social media graphics, banners, cards, presentations, logos, and any styled text content with complete creative control and iterative refinement.';
}
getAgentGuidance() {
return `
## 🎨 AI-POWERED TEXT-TO-IMAGE GENERATION
### What This Tool Does:
- Generates custom HTML layouts using AI based on your design requirements
- Uses Tailwind CSS for modern, responsive styling
- Creates high-quality images via browser rendering
- Perfect for social media, presentations, marketing materials, and creative content
### How to Use:
1. **Describe the design**: Provide clear instructions about the visual style, layout, colors, and mood
2. **Specify content**: Include the text content to be rendered
3. **Set dimensions**: Choose appropriate width/height for your use case
4. **Let AI create**: The tool generates custom HTML/CSS tailored to your requirements
### Design Description Examples:
- "Modern minimalist card with gradient background and bold typography"
- "Retro 80s neon style with synthwave colors and glowing text effects"
- "Professional business card layout with clean typography and subtle shadows"
- "Instagram story format with vibrant colors and emoji integration"
- "Technical documentation style with code syntax highlighting"
- "Vintage poster design with distressed textures and classic fonts"
### Best Practices:
- Be specific about visual style, colors, and mood
- Mention target platform (Instagram, Twitter, presentation, etc.)
- Include any special requirements (gradients, shadows, icons, etc.)
- Specify text hierarchy (title, subtitle, body text)
- Consider readability and contrast
### AI Capabilities:
- Generates complete HTML using ONLY Tailwind CSS classes
- Creates responsive, modern layouts with Tailwind utilities
- Handles complex styling (gradients, shadows, animations) using Tailwind's built-in classes
- Adapts to any design style or theme using Tailwind's comprehensive utility system
- Ensures proper text formatting and readability with Tailwind typography classes
- Uses Tailwind CDN and web fonts - NO custom CSS or inline styles
### Output:
- High-quality PNG images
- Custom dimensions as specified
- Professional browser rendering
- Optimized file sizes
- **HTML template files** saved alongside images for reuse
## 🔄 ITERATIVE DESIGN WORKFLOW (IMPORTANT FOR AGENTS!)
### Template Management:
- **Every generation saves TWO files**: image.png AND image.html
- **HTML files are reusable templates** that can be edited and re-rendered
- **Use file tools to modify HTML**, then re-render instantly without AI
### Agent Workflow Example:
\`\`\`
1. GENERATE: text_to_image(text="Hello World", design_prompt="blue gradient", output_path="design.png")
→ Creates: design.png + design.html
2. EDIT: Use file_write_tool to modify design.html:
- Change colors: class="bg-blue-500" → class="bg-red-500"
- Update fonts: class="font-sans" → class="font-serif"
- Modify layout: Add borders, shadows, etc.
3. RE-RENDER: text_to_image(html_file_path="design.html", output_path="variation.png")
→ Creates: variation.png instantly (no AI, <2 seconds)
\`\`\`
### Performance Modes:
- **Regular mode**: Loads HTML file, processes text, renders image
- **HTML-only mode**: html_only=true for maximum performance (no text processing)
### When to Use Each Mode:
- **First generation**: Use design_prompt to create initial template
- **Iterations**: Use html_file_path to reuse and modify existing templates
- **Maximum speed**: Use html_only=true when you don't need text processing
`;
}
getParameters() {
return [
{
name: 'text',
type: 'string',
description: 'The text content to render as an image. Can include basic formatting and line breaks. Not required in html_only mode.',
required: false
},
{
name: 'output_path',
type: 'string',
description: 'Output file path for the generated image (relative to project root). Should end with .png',
required: true
},
{
name: 'design_prompt',
type: 'string',
description: 'Detailed description of the desired visual design, style, layout, colors, and mood. Be specific about the visual aesthetic you want. Not required if html_file_path is provided.',
required: false
},
{
name: 'html_file_path',
type: 'string',
description: 'Path to existing HTML file to use for rendering (relative to project root). Alternative to design_prompt for iterating on existing designs.',
required: false
},
{
name: 'html_only',
type: 'boolean',
description: 'HTML-only mode: Skip all text processing and render HTML file directly. Requires html_file_path. Maximum performance for pure HTML rendering.',
required: false,
default: false
},
{
name: 'width',
type: 'number',
description: 'Image width in pixels (default: 1200)',
required: false,
default: 1200
},
{
name: 'height',
type: 'number',
description: 'Image height in pixels (default: 630)',
required: false,
default: 630
},
{
name: 'transparent',
type: 'boolean',
description: 'Generate PNG with transparent background instead of solid background (default: false)',
required: false,
default: false
}
];
}
async execute(parameters) {
// Validate parameters
const validationError = this.validateParameters(parameters);
if (validationError) {
return validationError;
}
const { text, output_path, design_prompt, html_file_path, html_only = false, width = 1200, height = 630, transparent = false } = parameters;
try {
// Validate text content (not required in HTML-only mode)
if (!html_only && (!text || typeof text !== 'string' || !text.trim())) {
return {
success: false,
error: 'Text content cannot be empty (unless using html_only mode)'
};
}
// Validate that either design_prompt or html_file_path is provided
if (!design_prompt && !html_file_path) {
return {
success: false,
error: 'Either design_prompt or html_file_path must be provided'
};
}
// Validate design prompt if provided
if (design_prompt && (typeof design_prompt !== 'string' || !design_prompt.trim())) {
return {
success: false,
error: 'Design prompt must be a non-empty string when provided'
};
}
// Validate html_file_path if provided
if (html_file_path && (typeof html_file_path !== 'string' || !html_file_path.trim())) {
return {
success: false,
error: 'HTML file path must be a non-empty string when provided'
};
}
// Validate html_only mode
if (html_only && !html_file_path) {
return {
success: false,
error: 'HTML-only mode requires html_file_path to be provided'
};
}
// HTML-only mode: optimized path for pure HTML rendering
if (html_only) {
return await this.renderHtmlOnly(html_file_path, output_path, width, height, transparent);
}
// Validate and resolve output path
const pathValidation = await this.validateAndResolveOutputPath(output_path);
if (!pathValidation.success) {
return {
success: false,
error: pathValidation.error
};
}
// Get HTML content either from file or generate via AI
let htmlContent;
let htmlFilePath;
let isFromFile = false;
if (html_file_path) {
// Load HTML from existing file
console.log(`📂 Loading HTML from file: "${html_file_path}"`);
const htmlLoadResult = await this.loadHtmlFromFile(html_file_path);
if (!htmlLoadResult.success) {
return {
success: false,
error: htmlLoadResult.error
};
}
htmlContent = htmlLoadResult.html;
htmlFilePath = htmlLoadResult.resolvedPath;
isFromFile = true;
}
else {
// Generate custom HTML using AI
console.log(`🤖 Generating custom design: "${design_prompt}"`);
const customTemplateResult = await this.generateCustomTemplate(design_prompt, text, width, height, transparent);
if (!customTemplateResult.success) {
return {
success: false,
error: customTemplateResult.error
};
}
htmlContent = customTemplateResult.html;
// Save HTML file alongside PNG
htmlFilePath = await this.saveHtmlFile(pathValidation.resolvedPath, htmlContent, {
text,
design_prompt: design_prompt,
width,
height,
timestamp: new Date().toISOString()
});
}
// Get browser service and render to image
console.log(`📝 Generating AI-designed text-to-image`);
console.log(`📐 Dimensions: ${width}x${height}`);
console.log(`🎨 Design: ${design_prompt}`);
const browserService = await BrowserServiceManager.getInstance();
await browserService.renderToImage(htmlContent, {
width,
height,
outputPath: pathValidation.resolvedPath,
transparent
});
// Get file stats
const fileStats = await fs.stat(pathValidation.resolvedPath);
const fileSize = fileStats.size;
const htmlFileStats = await fs.stat(htmlFilePath);
const htmlFileSize = htmlFileStats.size;
return {
success: true,
data: {
output_file_path: path.relative(this.baseDir, pathValidation.resolvedPath),
absolute_path: pathValidation.resolvedPath,
file_size: fileSize,
html_file_path: path.relative(this.baseDir, htmlFilePath),
html_absolute_path: htmlFilePath,
html_file_size: htmlFileSize,
design_prompt: design_prompt || 'Loaded from HTML file',
html_source: isFromFile ? 'loaded_from_file' : 'ai_generated',
dimensions: `${width}x${height}`,
text_length: text.length,
mime_type: 'image/png'
},
message: isFromFile
? `Text image generated successfully from existing HTML template. Image saved to: ${path.relative(this.baseDir, pathValidation.resolvedPath)} (${this.formatFileSize(fileSize)}) using HTML: ${path.relative(this.baseDir, htmlFilePath)} (${this.formatFileSize(htmlFileSize)})`
: `Text image generated successfully using AI-generated design. Saved to: ${path.relative(this.baseDir, pathValidation.resolvedPath)} (${this.formatFileSize(fileSize)}) and HTML to: ${path.relative(this.baseDir, htmlFilePath)} (${this.formatFileSize(htmlFileSize)})`
};
}
catch (error) {
return {
success: false,
error: `Failed to generate text image: ${error.message}`
};
}
}
/**
* Generate HTML content based on template and parameters
*/
async generateHTML(options) {
if (options.template === 'custom') {
// Use custom HTML directly
return this.processTemplate(options.custom_html, options);
}
// Load template file
const templatePath = path.join(__dirname, 'templates', `${options.template}.html`);
try {
const templateContent = await fs.readFile(templatePath, 'utf-8');
return this.processTemplate(templateContent, options);
}
catch (error) {
throw new Error(`Failed to load template "${options.template}": ${error.message}`);
}
}
/**
* Process template by replacing placeholders with actual values
*/
processTemplate(template, options) {
let processedTemplate = template;
// Replace all template variables
const replacements = {
'{{text}}': this.escapeHtml(options.text),
'{{width}}': options.width.toString(),
'{{height}}': options.height.toString(),
'{{background_color}}': options.background_color,
'{{text_color}}': options.text_color,
'{{font_family}}': options.font_family,
'{{font_size}}': options.font_size.toString(),
'{{padding}}': options.padding.toString(),
'{{custom_css}}': options.custom_css
};
// Add custom variables
for (const [key, value] of Object.entries(options.variables)) {
const placeholder = `{{${key}}}`;
replacements[placeholder] = this.escapeHtml(String(value));
}
// Replace all placeholders
for (const [placeholder, value] of Object.entries(replacements)) {
processedTemplate = processedTemplate.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value);
}
// Handle simple conditionals like {{#if variable}}content{{/if}}
processedTemplate = this.processConditionals(processedTemplate, options.variables);
return processedTemplate;
}
/**
* Process simple conditionals in templates
*/
processConditionals(template, variables) {
// Handle {{#if variable}}content{{/if}} patterns
const conditionalRegex = /\{\{#if\s+(\w+)\}\}(.*?)\{\{\/if\}\}/gs;
return template.replace(conditionalRegex, (_match, variableName, content) => {
const variableValue = variables[variableName];
// Show content if variable exists and is truthy
if (variableValue && variableValue !== '' && variableValue !== '0' && variableValue !== 'false') {
return content;
}
return '';
});
}
/**
* Escape HTML special characters to prevent XSS
*/
escapeHtml(text) {
const htmlEscapes = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, (match) => htmlEscapes[match]);
}
/**
* Generate custom template using LLM
*/
async generateCustomTemplate(designPrompt, text, width, height, transparent = false) {
try {
// Try to get a configured provider, fallback to openai
let llm;
try {
llm = await LLMFactory.getConfiguredProvider();
if (!llm) {
llm = LLMFactory.getProvider('openai');
}
}
catch (error) {
llm = LLMFactory.getProvider('openai');
}
const prompt = `Create a complete HTML document for text-to-image rendering:
DESIGN: ${designPrompt}
TEXT: ${text}
SIZE: ${width}x${height}px
TRANSPARENT: ${transparent ? 'YES - Use transparent background' : 'NO - Use solid background'}
Requirements:
- Complete HTML5 with DOCTYPE
- Include Tailwind CSS CDN
- USE ONLY TAILWIND CSS CLASSES - NO RAW CSS OR CUSTOM STYLES
- Do NOT write any custom CSS in <style> tags or inline styles
- Use only Tailwind utility classes for all styling (colors, fonts, spacing, etc.)
- Exact dimensions: ${width}x${height}px using Tailwind classes
- Professional typography and layout using Tailwind typography classes
- Match the design requirements exactly using only Tailwind utilities
- Ensure excellent readability with Tailwind color and contrast classes
- If you need custom effects, use Tailwind's built-in utilities like gradients, shadows, transforms
${transparent ? '- TRANSPARENT BACKGROUND: Do NOT add background colors to the body or main container. Use transparent/no background for the main container so the image will have a transparent background.' : '- SOLID BACKGROUND: Use appropriate background colors for the design.'}
EXAMPLE - Simple Quote Card (Tailwind-only):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="m-0 p-0" style="width: 800px; height: 600px;">
<div class="w-full h-full bg-gradient-to-br from-blue-50 to-indigo-100 p-8 flex items-center justify-center">
<div class="bg-white rounded-xl shadow-lg p-6 max-w-md">
<blockquote class="text-gray-800 text-lg font-medium mb-4">
"Your quote text here"
</blockquote>
<p class="text-gray-600 text-sm">— Author Name</p>
</div>
</div>
</body>
</html>
IMPORTANT: Do not include any <style> tags or style="" attributes. Use ONLY Tailwind CSS classes like the example above.
Return ONLY the HTML code, no explanations or markdown.`;
const response = await llm.executePrompt(prompt, {
temperature: 0.7,
maxTokens: 8000 // Further increased token limit for complete HTML generation
});
// Basic validation and cleanup
const cleanedHtml = this.validateAndCleanGeneratedTemplate(response.content);
return {
success: true,
html: cleanedHtml,
width,
height
};
}
catch (error) {
console.log(`⚠️ LLM generation failed, using fallback template: ${error.message}`);
// Fallback: create a simple custom template based on the prompt
const fallbackHtml = this.createFallbackTemplate(designPrompt, text, width, height, transparent);
return {
success: true,
html: fallbackHtml,
width,
height
};
}
}
/**
* Create a fallback template when LLM generation fails
*/
createFallbackTemplate(designPrompt, text, width, height, transparent = false) {
// Analyze the prompt for style hints with more comprehensive pattern matching
const prompt = designPrompt.toLowerCase();
// Color analysis
const hasBlue = /blue/i.test(designPrompt);
const hasRed = /red/i.test(designPrompt);
const hasGreen = /green/i.test(designPrompt);
const hasYellow = /yellow/i.test(designPrompt);
const hasPurple = /purple/i.test(designPrompt);
const hasPink = /pink/i.test(designPrompt);
// Style analysis
const isRetro = /retro|80s|synthwave|neon|vintage/i.test(designPrompt);
const isMinimal = /minimal|clean|simple|modern/i.test(designPrompt);
const isGradient = /gradient|colorful|vibrant/i.test(designPrompt);
const isDark = /dark|black|night/i.test(designPrompt);
const hasRounded = /rounded|round/i.test(designPrompt);
const hasBorder = /border/i.test(designPrompt);
const hasShadow = /shadow/i.test(designPrompt);
let bgClasses = transparent ? '' : 'bg-gray-100';
let textClasses = transparent ? 'text-gray-900' : 'text-gray-900';
let containerClasses = 'p-8';
let shadowClasses = '';
let borderClasses = '';
// Build background based on colors and style (skip if transparent)
if (!transparent) {
if (isGradient || hasBlue) {
if (hasBlue && hasYellow) {
bgClasses = 'bg-gradient-to-br from-blue-500 via-blue-600 to-blue-700';
}
else if (hasBlue) {
bgClasses = 'bg-gradient-to-br from-blue-400 via-blue-500 to-blue-600';
}
else if (isGradient) {
bgClasses = 'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500';
}
textClasses = 'text-white';
}
else if (isRetro) {
bgClasses = 'bg-gradient-to-br from-purple-600 via-pink-600 to-blue-600';
textClasses = 'text-white';
containerClasses = 'p-8 font-mono';
}
else if (isMinimal) {
bgClasses = 'bg-white';
textClasses = 'text-gray-800';
containerClasses = 'p-12';
}
else if (isDark) {
bgClasses = 'bg-gray-900';
textClasses = 'text-white';
containerClasses = 'p-8';
}
}
else {
// For transparent backgrounds, ensure good text contrast
textClasses = 'text-gray-900';
if (isDark) {
textClasses = 'text-white';
}
}
// Add shadow using Tailwind classes if requested
if (hasShadow) {
shadowClasses = 'drop-shadow-lg';
}
// Add border if requested
if (hasBorder) {
borderClasses = 'border-2 border-white border-opacity-30';
}
// Add rounded corners if requested
const roundedClasses = hasRounded ? 'rounded-lg' : '';
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Generated Text Image</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
'inter': ['Inter', 'sans-serif']
}
}
}
}
</script>
</head>
<body class="m-0 p-0 font-inter" style="width: ${width}px; height: ${height}px;">
<div class="w-full h-full ${bgClasses} ${containerClasses} ${roundedClasses} ${borderClasses} flex items-center justify-center">
<div class="${textClasses} text-center max-w-full">
<div class="text-2xl md:text-3xl lg:text-4xl font-bold leading-tight whitespace-pre-line ${shadowClasses}">
${text}
</div>
</div>
</div>
</body>
</html>`;
}
/**
* Validate and clean generated HTML template
*/
validateAndCleanGeneratedTemplate(html) {
// Remove markdown code blocks if present
let cleaned = html.replace(/```html\n?/g, '').replace(/```\n?/g, '');
// Ensure it starts with <!DOCTYPE html>
if (!cleaned.includes('<!DOCTYPE html>')) {
cleaned = '<!DOCTYPE html>\n' + cleaned;
}
// Basic security: ensure no script tags (except Tailwind CDN and config)
cleaned = cleaned.replace(/<script(?![^>]*tailwind)[^>]*>.*?<\/script>/gis, '');
// GUARDRAIL: Remove any <style> tags to enforce Tailwind-only approach
cleaned = cleaned.replace(/<style[^>]*>.*?<\/style>/gis, '');
// GUARDRAIL: Remove inline style attributes (except for body dimensions which are necessary)
cleaned = cleaned.replace(/style="[^"]*"/gi, (match) => {
// Keep style attributes that only contain width/height for body element
if (match.includes('width:') && match.includes('height:') && !match.includes('color') && !match.includes('background') && !match.includes('font')) {
return match;
}
return '';
});
// Log warning if raw CSS was detected and removed
if (html !== cleaned) {
console.log('⚠️ Raw CSS detected and removed from generated HTML. Using Tailwind-only approach.');
}
return cleaned.trim();
}
/**
* Get preset configuration for social media platforms
*/
getPresetConfiguration(preset) {
const presets = {
'instagram-square': { width: 1080, height: 1080, template: 'card', fontSize: 48 },
'instagram-story': { width: 1080, height: 1920, template: 'card', fontSize: 52 },
'twitter-post': { width: 1200, height: 675, template: 'card', fontSize: 36 },
'linkedin-post': { width: 1200, height: 630, template: 'card', fontSize: 32 },
'facebook-cover': { width: 1200, height: 315, template: 'banner', fontSize: 28 },
'youtube-thumbnail': { width: 1280, height: 720, template: 'card', fontSize: 42 },
'pinterest-pin': { width: 1000, height: 1500, template: 'card', fontSize: 38 }
};
if (!presets[preset]) {
return {
success: false,
error: `Invalid preset "${preset}". Available presets: ${Object.keys(presets).join(', ')}`
};
}
return {
success: true,
...presets[preset]
};
}
/**
* Format file size in human readable format
*/
formatFileSize(bytes) {
if (bytes === 0)
return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Validate and resolve output path with security checks
* (Copied from ImageGenerationTool for consistency)
*/
async validateAndResolveOutputPath(outputPath) {
try {
// Normalize the path to prevent directory traversal
const normalizedPath = path.normalize(outputPath);
// Check for directory traversal attempts
if (normalizedPath.includes('..') || path.isAbsolute(normalizedPath)) {
return {
success: false,
error: 'Output path must be relative and cannot contain ".." or be absolute for security reasons'
};
}
// Resolve full path
const fullPath = path.join(this.baseDir, normalizedPath);
// Ensure the resolved path is still within the base directory
const relativePath = path.relative(this.baseDir, fullPath);
if (relativePath.startsWith('..')) {
return {
success: false,
error: 'Output path resolves outside of project directory'
};
}
// Create parent directories if they don't exist
const parentDir = path.dirname(fullPath);
await fs.mkdir(parentDir, { recursive: true });
return {
success: true,
resolvedPath: fullPath
};
}
catch (error) {
return {
success: false,
error: `Failed to resolve output path: ${error.message}`
};
}
}
/**
* Save HTML file alongside the PNG output with metadata comments
*/
async saveHtmlFile(pngFilePath, htmlContent, metadata) {
// Generate HTML file path by replacing .png extension with .html
const htmlFilePath = pngFilePath.replace(/\.png$/i, '.html');
// Add metadata comments to the HTML content
const htmlWithMetadata = this.addMetadataToHtml(htmlContent, metadata);
// Save the HTML file
await fs.writeFile(htmlFilePath, htmlWithMetadata, 'utf-8');
console.log(`💾 HTML template saved: ${path.relative(this.baseDir, htmlFilePath)}`);
return htmlFilePath;
}
/**
* Add metadata comments to HTML content
*/
addMetadataToHtml(htmlContent, metadata) {
const metadataComment = `<!--
Generated by Contaigents TextToImageTool
Timestamp: ${metadata.timestamp}
Text Content: ${metadata.text}
Design Prompt: ${metadata.design_prompt}
Dimensions: ${metadata.width}x${metadata.height}
-->
`;
// Insert metadata comment after DOCTYPE but before <html>
if (htmlContent.includes('<!DOCTYPE html>')) {
return htmlContent.replace('<!DOCTYPE html>', `<!DOCTYPE html>\n${metadataComment}`);
}
else {
// If no DOCTYPE, add at the beginning
return metadataComment + htmlContent;
}
}
/**
* Load HTML content from existing file
*/
async loadHtmlFromFile(htmlFilePath) {
try {
// Validate and resolve HTML file path
const pathValidation = await this.validateAndResolveInputPath(htmlFilePath);
if (!pathValidation.success) {
return {
success: false,
error: pathValidation.error
};
}
// Read HTML file content
const htmlContent = await fs.readFile(pathValidation.resolvedPath, 'utf-8');
// Basic validation that it's HTML content
if (!htmlContent.includes('<html') && !htmlContent.includes('<!DOCTYPE')) {
return {
success: false,
error: 'File does not appear to contain valid HTML content'
};
}
console.log(`📂 Loaded HTML template: ${path.relative(this.baseDir, pathValidation.resolvedPath)} (${htmlContent.length} chars)`);
return {
success: true,
html: htmlContent,
resolvedPath: pathValidation.resolvedPath
};
}
catch (error) {
return {
success: false,
error: `Failed to load HTML file: ${error.message}`
};
}
}
/**
* Validate and resolve input file path (similar to output path but for reading)
*/
async validateAndResolveInputPath(inputPath) {
try {
// Normalize the path to prevent directory traversal
const normalizedPath = path.normalize(inputPath);
// Check for directory traversal attempts
if (normalizedPath.includes('..') || path.isAbsolute(normalizedPath)) {
return {
success: false,
error: 'Input path must be relative and cannot contain ".." or be absolute for security reasons'
};
}
// Resolve full path
const fullPath = path.join(this.baseDir, normalizedPath);
// Ensure the resolved path is still within the base directory
const relativePath = path.relative(this.baseDir, fullPath);
if (relativePath.startsWith('..')) {
return {
success: false,
error: 'Input path resolves outside of project directory'
};
}
// Check if file exists
try {
await fs.access(fullPath, fs.constants.R_OK);
}
catch (error) {
return {
success: false,
error: `HTML file does not exist or is not readable: ${inputPath}`
};
}
return {
success: true,
resolvedPath: fullPath
};
}
catch (error) {
return {
success: false,
error: `Failed to resolve input path: ${error.message}`
};
}
}
/**
* HTML-only rendering mode: optimized path for pure HTML-to-image conversion
* Skips all text processing and LLM generation for maximum performance
*/
async renderHtmlOnly(htmlFilePath, outputPath, width, height, transparent = false) {
try {
console.log(`⚡ HTML-only mode: Maximum performance rendering`);
console.log(`📂 HTML: ${htmlFilePath}`);
console.log(`📐 Dimensions: ${width}x${height}`);
// Validate and resolve HTML file path
const htmlPathValidation = await this.validateAndResolveInputPath(htmlFilePath);
if (!htmlPathValidation.success) {
return {
success: false,
error: htmlPathValidation.error
};
}
// Validate and resolve output path
const outputPathValidation = await this.validateAndResolveOutputPath(outputPath);
if (!outputPathValidation.success) {
return {
success: false,
error: outputPathValidation.error
};
}
// Load HTML content directly
const htmlContent = await fs.readFile(htmlPathValidation.resolvedPath, 'utf-8');
// Basic HTML validation
if (!htmlContent.includes('<html') && !htmlContent.includes('<!DOCTYPE')) {
return {
success: false,
error: 'File does not appear to contain valid HTML content'
};
}
// Optimized browser rendering
console.log(`🚀 Rendering HTML to image (optimized path)`);
const browserService = await BrowserServiceManager.getInstance();
await browserService.renderToImage(htmlContent, {
width,
height,
outputPath: outputPathValidation.resolvedPath,
transparent
});
// Get file stats
const fileStats = await fs.stat(outputPathValidation.resolvedPath);
const fileSize = fileStats.size;
const htmlFileStats = await fs.stat(htmlPathValidation.resolvedPath);
const htmlFileSize = htmlFileStats.size;
return {
success: true,
data: {
output_file_path: path.relative(this.baseDir, outputPathValidation.resolvedPath),
absolute_path: outputPathValidation.resolvedPath,
file_size: fileSize,
html_file_path: path.relative(this.baseDir, htmlPathValidation.resolvedPath),
html_absolute_path: htmlPathValidation.resolvedPath,
html_file_size: htmlFileSize,
design_prompt: 'HTML-only mode (no text processing)',
html_source: 'html_only_mode',
dimensions: `${width}x${height}`,
text_length: 0,
mime_type: 'image/png',
rendering_mode: 'html_only'
},
message: `HTML-only rendering complete! Image saved to: ${path.relative(this.baseDir, outputPathValidation.resolvedPath)} (${this.formatFileSize(fileSize)}) from HTML: ${path.relative(this.baseDir, htmlPathValidation.resolvedPath)} (${this.formatFileSize(htmlFileSize)})`
};
}
catch (error) {
return {
success: false,
error: `Failed to render HTML-only: ${error.message}`
};
}
}
/**
* List available HTML templates in the project directory
* Helps agents discover and manage existing templates
*/
async listAvailableTemplates() {
try {
console.log(`📋 Discovering HTML templates in project directory...`);
// Find all HTML files in the project directory
const htmlFiles = await this.findHtmlFiles(this.baseDir);
if (htmlFiles.length === 0) {
return {
success: true,
data: {
templates: [],
count: 0,
message: 'No HTML templates found in project directory'
},
message: 'No HTML templates found in project directory. Create some templates first using the text-to-image tool.'
};
}
// Analyze each template for metadata
const templates = await Promise.all(htmlFiles.map(async (filePath) => {
const metadata = await this.analyzeTemplate(filePath);
return {
file_path: path.relative(this.baseDir, filePath),
absolute_path: filePath,
...metadata
};
}));
return {
success: true,
data: {
templates,
count: templates.length,
project_directory: this.baseDir
},
message: `Found ${templates.length} HTML template(s) in project directory. Use --html-file-path to render any of these templates.`
};
}
catch (error) {
return {
success: false,
error: `Failed to list templates: ${error.message}`
};
}
}
/**
* Find all HTML files in a directory recursively
*/
async findHtmlFiles(directory) {
const htmlFiles = [];
const scanDirectory = async (dir, depth = 0) => {
// Limit recursion depth to avoid infinite loops
if (depth > 3)
return;
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip hidden directories and node_modules
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
continue;
}
if (entry.isDirectory()) {
await scanDirectory(fullPath, depth + 1);
}
else if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
htmlFiles.push(fullPath);
}
}
}
catch (error) {
// Skip directories we can't read
console.warn(`Warning: Could not read directory ${dir}: ${error.message}`);
}
};
await scanDirectory(directory);
return htmlFiles.sort();
}
/**
* Analyze an HTML template file for metadata
*/
async analyzeTemplate(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
const stats = await fs.stat(filePath);
// Extract metadata from comments if present
const metadataMatch = content.match(/<!--\s*Generated by Contaigents TextToImageTool\s*([\s\S]*?)\s*-->/);
let metadata = {};
if (metadataMatch) {
const metadataText = metadataMatch[1];
const timestampMatch = metadataText.match(/Timestamp:\s*(.+)/);
const designMatch = metadataText.match(/Design Prompt:\s*(.+)/);
const dimensionsMatch = metadataText.match(/Dimensions:\s*(.+)/);
const textMatch = metadataText.match(/Text Content:\s*(.+)/);
metadata = {
created_date: timestampMatch ? timestampMatch[1].trim() : undefined,
design_prompt: designMatch ? designMatch[1].trim() : undefined,
dimensions: dimensionsMatch ? dimensionsMatch[1].trim() : undefined,
text_content: textMatch ? textMatch[1].trim() : undefined,
has_metadata: true
};
}
else {
metadata = {
has_metadata: false
};
}
return {
file_size: stats.size,
...metadata
};
}
catch (error) {
return {
file_size: 0,
has_metadata: false
};
}
}
}