From e0d54bd1b6ccc3bbf9eaaa38a5ebd18e5e0f66c1 Mon Sep 17 00:00:00 2001 From: Sven Olderaan Date: Sun, 16 Mar 2025 16:09:01 +0100 Subject: [PATCH] Add character-based chat bubble system with new directory structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement 5:1 ratio comic-style chat bubbles with character images - Create modular design with character-specific assets - Organize assets in /characters/{name}/ directories - Add fallback system using Example character - Support both speech and thought bubble types - Maintain backward compatibility with style parameter 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Generator.md | 115 +++++++++ characters/Example/background.png | Bin 0 -> 3421 bytes characters/Example/character.png | Bin 0 -> 4209 bytes characters/Example/speech.png | Bin 0 -> 4105 bytes characters/Example/thought.png | Bin 0 -> 7382 bytes image.php | 394 ++++++++++++++++++++---------- 6 files changed, 380 insertions(+), 129 deletions(-) create mode 100644 Generator.md create mode 100644 characters/Example/background.png create mode 100644 characters/Example/character.png create mode 100644 characters/Example/speech.png create mode 100644 characters/Example/thought.png diff --git a/Generator.md b/Generator.md new file mode 100644 index 0000000..cef2bf1 --- /dev/null +++ b/Generator.md @@ -0,0 +1,115 @@ +# SillyBubble Image Generator - Design Document + +## Component Architecture + +The SillyBubble image generator creates comic-style chat visualizations with a modular component system. The final image has a 5:1 width-to-height ratio, composed of layered elements. + +### Core Components + +1. **Background Layer** + - Full 5:1 ratio base image for setting style/mood + - Can be themed (e.g., fantasy, sci-fi, cozy, etc.) + - Provides consistent canvas for all other elements + +2. **Character Layer (Chibi)** + - Positioned on the left side of the image + - Various character images with different expressions/poses + - Takes approximately 20% of the total width + - Named by character (e.g., "bianca.png", "ruby.png") + +3. **Bubble Layer** + - Semi-transparent speech bubble + - Positioned to the right of the character + - Includes a pointer/tail connecting to the character + - Takes approximately 70% of the total width + - Various styles (rounded, square, cloud, thought, etc.) + +4. **Text Layer** + - Dynamically rendered text content + - Positioned within the bubble boundaries + - Supports word-wrapping and styling + - Font options compatible with the overall theme + +## Implementation Approach + +### File Structure and Naming Convention +``` +/characters/ - Main directory for all characters + /Example/ - Example character (fallback) + background.png - Background image + character.png - Character image + speech.png - Speech bubble + thought.png - Thought bubble + /Bianca/ - Another character + background.png - Background image + character.png - Character image + speech.png - Speech bubble + thought.png - Thought bubble +/fonts/*.ttf - Font files +``` + +Each character has their own directory containing all assets. If a specific character's asset is missing, the system will fall back to the Example character's corresponding asset. + +### Image Dimensions +- Total Image: 2000×400px (5:1 ratio) +- Background: Full 2000×400px canvas +- Character: ~400×400px (20% of width) +- Bubble: ~1400×300px (70% of width) +- Text Area: ~1300×250px (inside bubble) +- Remaining 10% (200px width) for margins and spacing + +### Parameter System +The enhanced image.php will accept: +- `q`: Text content (required) +- `character`: Character to use (e.g., "bianca") - if not provided, defaults to "Example" +- `bubble_type`: "speech" or "thought" (defaults to "speech") +- `style`: Legacy parameter, can be used instead of character parameter + +**Backward Compatibility**: +- If only `style` is provided (no `character`), the script will use the style value as the character name +- This ensures the SillyTavern extension doesn't need modification + +### Image Composition Process +1. Load or create background layer (full canvas) +2. Check if character exists and overlay on left side +3. Position and overlay appropriate bubble template +4. Calculate text boundaries within bubble +5. Render text with proper wrapping and styling +6. Output final composed image + +### Dynamic Bubble Generation +- If no bubble template exists, dynamically draw a bubble +- Support both template-based and on-the-fly bubble generation +- Ensure proper connection between character and bubble + +## Visual Representation + +``` +FINAL COMPOSITION (5:1 ratio): ++--------------------------------------------------------------------------------------+ +| | +| +--------+ +--------------------------------------------------------------+ | +| | | | | | +| | CHIBI |<---+ TEXT CONTENT | | +| | | | | | +| +--------+ +--------------------------------------------------------------+ | +| | ++--------------------------------------------------------------------------------------+ +``` + +## Feature Roadmap + +1. **Basic Implementation** + - Support for character-based styling + - Simple bubble positioning + - Proper text wrapping + +2. **Enhanced Features** + - Multiple character positions (left/right) + - Various bubble styles + - Expression selection for characters + +3. **Advanced Features** + - Multiple characters in one image + - Animated GIF output option + - Theme-based text styling \ No newline at end of file diff --git a/characters/Example/background.png b/characters/Example/background.png new file mode 100644 index 0000000000000000000000000000000000000000..eb8fc0699560a8890d0dd340f481fba706226b50 GIT binary patch literal 3421 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 ptj;8vcCY~CYa0AtjJ$B=)Yd!hKLPhpL-YUu literal 0 HcmV?d00001 diff --git a/characters/Example/speech.png b/characters/Example/speech.png new file mode 100644 index 0000000000000000000000000000000000000000..da88c1789dc3e00adafaeb37c2a0816c6f57bacf GIT binary patch literal 4105 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 X^t=ezR~B_3Qrv1us{N literal 0 HcmV?d00001 diff --git a/characters/Example/thought.png b/characters/Example/thought.png new file mode 100644 index 0000000000000000000000000000000000000000..b82e3434bdf14a266d13971fd88b463ea5bc9086 GIT binary patch literal 7382 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 [ 'bg_color' => [245, 245, 245], 'text_color' => [50, 50, 50], 'border_color' => [200, 200, 200], - 'padding' => 20, - 'rounded' => 15, - 'font_size' => 14, - 'max_width' => 600, - 'line_height' => 20, - 'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed + 'font_size' => 24, // Increased for higher resolution + 'line_height' => 30, + 'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf', ], 'modern' => [ 'bg_color' => [66, 133, 244], 'text_color' => [255, 255, 255], 'border_color' => [59, 120, 220], - 'padding' => 20, - 'rounded' => 20, - 'font_size' => 14, - 'max_width' => 600, - 'line_height' => 20, - 'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed + 'font_size' => 24, + 'line_height' => 30, + 'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf', ], 'retro' => [ 'bg_color' => [255, 204, 102], 'text_color' => [51, 51, 51], 'border_color' => [204, 153, 0], - 'padding' => 20, - 'rounded' => 5, - 'font_size' => 14, - 'max_width' => 600, - 'line_height' => 20, - 'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed + 'font_size' => 24, + 'line_height' => 30, + 'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf', ], 'minimal' => [ 'bg_color' => [255, 255, 255], 'text_color' => [0, 0, 0], 'border_color' => [220, 220, 220], - 'padding' => 15, - 'rounded' => 0, - 'font_size' => 14, - 'max_width' => 600, - 'line_height' => 20, - 'font' => __DIR__ . '/fonts/arial.ttf', // Adjust path as needed + 'font_size' => 24, + 'line_height' => 30, + 'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf', ], ]; -// Use default style if specified style doesn't exist -if (!isset($styles[$style])) { - $style = 'default'; -} +// Set default styling +$config = [ + 'bg_color' => [245, 245, 245], + 'text_color' => [50, 50, 50], + 'border_color' => [200, 200, 200], + 'font_size' => 24, + 'line_height' => 30, + 'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf', +]; -// Get style settings -$config = $styles[$style]; +// Ensure required directories exist +$dirs = [ + __DIR__ . '/characters/Example', + __DIR__ . '/fonts' +]; -// Check for fonts directory and create if it doesn't exist -$fontsDir = __DIR__ . '/fonts'; -if (!is_dir($fontsDir)) { - mkdir($fontsDir, 0755, true); +foreach ($dirs as $dir) { + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } } // Fallback to a built-in font if the specified font file doesn't exist if (!file_exists($config['font'])) { // Try to find any TTF font in the fonts directory - $foundFont = false; - if (is_dir($fontsDir)) { - $fontFiles = glob($fontsDir . '/*.ttf'); - if (!empty($fontFiles)) { - $config['font'] = $fontFiles[0]; - $foundFont = true; - } - } - - // If no TTF font found, use built-in font - if (!$foundFont) { + $fontFiles = glob(__DIR__ . '/fonts/*.ttf'); + if (!empty($fontFiles)) { + $config['font'] = $fontFiles[0]; + } else { $config['use_builtin_font'] = true; } } -// Create a temporary image to calculate text dimensions -$tempImage = imagecreatetruecolor(100, 100); -$textColor = imagecolorallocate($tempImage, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]); +// Create the canvas +$canvas = imagecreatetruecolor($canvasWidth, $canvasHeight); + +// Set default white background +$whiteColor = imagecolorallocate($canvas, 255, 255, 255); +imagefill($canvas, 0, 0, $whiteColor); + +// Set text color +$textColor = imagecolorallocate($canvas, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]); + +// Function to get image path with fallback to Example +function getAssetPath($assetType, $character) { + // Primary path in character's directory + $primaryPath = __DIR__ . '/characters/' . $character . '/' . $assetType . '.png'; + + // Fallback to Example character + $fallbackPath = __DIR__ . '/characters/Example/' . $assetType . '.png'; + + return file_exists($primaryPath) ? $primaryPath : $fallbackPath; +} + +// Check for and load the background image +$backgroundPath = getAssetPath('background', $character); +$backgroundLoaded = false; + +if (file_exists($backgroundPath)) { + $backgroundImage = imagecreatefrompng($backgroundPath); + if ($backgroundImage) { + // Resize if needed + $bgWidth = imagesx($backgroundImage); + $bgHeight = imagesy($backgroundImage); + + // Copy background to canvas, resizing if necessary + imagecopyresampled($canvas, $backgroundImage, 0, 0, 0, 0, $canvasWidth, $canvasHeight, $bgWidth, $bgHeight); + imagedestroy($backgroundImage); + $backgroundLoaded = true; + } +} + +// If no background was loaded, use a solid color background +if (!$backgroundLoaded) { + // Use legacy style background color if it exists + if (isset($styles[$style])) { + $bgColor = imagecolorallocate($canvas, + $styles[$style]['bg_color'][0], + $styles[$style]['bg_color'][1], + $styles[$style]['bg_color'][2] + ); + } else { + $bgColor = imagecolorallocate($canvas, 245, 245, 245); // Default light gray + } + imagefilledrectangle($canvas, 0, 0, $canvasWidth-1, $canvasHeight-1, $bgColor); +} + +// Check for and load the character image +$characterPath = getAssetPath('character', $character); +$characterLoaded = false; + +if (file_exists($characterPath)) { + $characterImage = imagecreatefrompng($characterPath); + if ($characterImage) { + // Enable alpha blending + imagesavealpha($characterImage, true); + + // Calculate position (center vertically, align left) + $charWidth = imagesx($characterImage); + $charHeight = imagesy($characterImage); + $charX = $bubbleMargin; + $charY = ($canvasHeight - $charHeight) / 2; + + // Copy character to canvas + imagecopy($canvas, $characterImage, $charX, $charY, 0, 0, $charWidth, $charHeight); + imagedestroy($characterImage); + $characterLoaded = true; + } +} + +// Check for and load the bubble image +$bubblePath = getAssetPath($bubble_type, $character); +$bubbleLoaded = false; + +if (file_exists($bubblePath)) { + $bubbleImage = imagecreatefrompng($bubblePath); + if ($bubbleImage) { + // Enable alpha blending + imagesavealpha($bubbleImage, true); + + // Calculate position (center vertically, align right of character) + $bubbleWidth = imagesx($bubbleImage); + $bubbleHeight = imagesy($bubbleImage); + $bubbleX = $charWidth + $bubbleMargin * 2; + $bubbleY = ($canvasHeight - $bubbleHeight) / 2; + + // Copy bubble to canvas + imagecopy($canvas, $bubbleImage, $bubbleX, $bubbleY, 0, 0, $bubbleWidth, $bubbleHeight); + imagedestroy($bubbleImage); + $bubbleLoaded = true; + } +} + +// If no bubble was loaded, draw a simple bubble +if (!$bubbleLoaded) { + // Calculate bubble dimensions and position + $bubbleX = $charWidth + $bubbleMargin * 2; + $bubbleY = $canvasHeight * 0.1; + $bubbleWidth = $canvasWidth - $bubbleX - $bubbleMargin; + $bubbleHeight = $canvasHeight * 0.8; + + // Determine bubble style based on legacy styles if available + if (isset($styles[$style])) { + $bubbleBgColor = imagecolorallocate($canvas, + $styles[$style]['bg_color'][0], + $styles[$style]['bg_color'][1], + $styles[$style]['bg_color'][2] + ); + $bubbleBorderColor = imagecolorallocate($canvas, + $styles[$style]['border_color'][0], + $styles[$style]['border_color'][1], + $styles[$style]['border_color'][2] + ); + } else { + $bubbleBgColor = imagecolorallocate($canvas, 245, 245, 245); // Default light gray + $bubbleBorderColor = imagecolorallocate($canvas, 200, 200, 200); // Default gray border + } + + // Draw bubble based on type + if ($bubble_type == 'thought') { + // Draw a thought bubble (rounded rectangle with smaller circles) + $radius = 40; + + // Main bubble + imagefilledrectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBgColor); + imagefilledrectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBgColor); + + // Corners + imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBgColor, IMG_ARC_PIE); + imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBgColor, IMG_ARC_PIE); + imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBgColor, IMG_ARC_PIE); + imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBgColor, IMG_ARC_PIE); + + // Draw small circles leading to character + $circleCount = 3; + $startX = $bubbleX - 20; + $startY = $bubbleY + $bubbleHeight/2 + 20; + + for ($i = 0; $i < $circleCount; $i++) { + $circleRadius = 15 - ($i * 4); + $circleX = $startX - ($i * 30); + $circleY = $startY + ($i * 20); + imagefilledellipse($canvas, $circleX, $circleY, $circleRadius*2, $circleRadius*2, $bubbleBgColor); + imageellipse($canvas, $circleX, $circleY, $circleRadius*2, $circleRadius*2, $bubbleBorderColor); + } + + // Border + imagerectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBorderColor); + imagerectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBorderColor); + imagearc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBorderColor); + imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBorderColor); + imagearc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBorderColor); + imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBorderColor); + + } else { + // Draw a speech bubble (rounded rectangle with a pointer) + $radius = 40; + + // Main bubble + imagefilledrectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBgColor); + imagefilledrectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBgColor); + + // Corners + imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBgColor, IMG_ARC_PIE); + imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBgColor, IMG_ARC_PIE); + imagefilledarc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBgColor, IMG_ARC_PIE); + imagefilledarc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBgColor, IMG_ARC_PIE); + + // Draw pointer + $pointerX = [$bubbleX, $bubbleX - 40, $bubbleX]; + $pointerY = [$bubbleY + $bubbleHeight/2 - 40, $bubbleY + $bubbleHeight/2, $bubbleY + $bubbleHeight/2 + 40]; + imagefilledpolygon($canvas, $pointerX, $pointerY, 3, $bubbleBgColor); + + // Border + imagerectangle($canvas, $bubbleX + $radius, $bubbleY, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight, $bubbleBorderColor); + imagerectangle($canvas, $bubbleX, $bubbleY + $radius, $bubbleX + $bubbleWidth, $bubbleY + $bubbleHeight - $radius, $bubbleBorderColor); + imagearc($canvas, $bubbleX + $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 180, 270, $bubbleBorderColor); + imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $radius, $radius * 2, $radius * 2, 270, 360, $bubbleBorderColor); + imagearc($canvas, $bubbleX + $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 90, 180, $bubbleBorderColor); + imagearc($canvas, $bubbleX + $bubbleWidth - $radius, $bubbleY + $bubbleHeight - $radius, $radius * 2, $radius * 2, 0, 90, $bubbleBorderColor); + + // Pointer border + imagepolygon($canvas, $pointerX, $pointerY, 3, $bubbleBorderColor); + } +} + +// Set text area dimensions +$textAreaX = $bubbleX + $bubblePadding; +$textAreaY = $bubbleY + $bubblePadding; +$textAreaWidth = $bubbleWidth - ($bubblePadding * 2); +$textAreaHeight = $bubbleHeight - ($bubblePadding * 2); // Word wrap the text $words = explode(' ', $text); $lines = []; $currentLine = ''; -$maxWidth = $config['max_width'] - (2 * $config['padding']); +$maxWidth = $textAreaWidth; foreach ($words as $word) { $testLine = $currentLine . ' ' . $word; @@ -130,92 +336,22 @@ if ($currentLine !== '') { $lines[] = $currentLine; } -// Calculate image dimensions -$lineCount = count($lines); -if (isset($config['use_builtin_font'])) { - $lineHeight = imagefontheight(5); - $textHeight = $lineHeight * $lineCount; - - // Find the longest line to determine width - $textWidth = 0; - foreach ($lines as $line) { - $lineWidth = imagefontwidth(5) * strlen($line); - $textWidth = max($textWidth, $lineWidth); - } -} else { - $lineHeight = $config['line_height']; - $textHeight = $lineHeight * $lineCount; - - // Find the longest line to determine width - $textWidth = 0; - foreach ($lines as $line) { - $bbox = imagettfbbox($config['font_size'], 0, $config['font'], $line); - $lineWidth = $bbox[2] - $bbox[0]; - $textWidth = max($textWidth, $lineWidth); - } -} - -// Add padding to dimensions -$imgWidth = min($config['max_width'], $textWidth + (2 * $config['padding'])); -$imgHeight = $textHeight + (2 * $config['padding']); - -// Create the bubble image -$image = imagecreatetruecolor($imgWidth, $imgHeight); - -// Allocate colors -$bgColor = imagecolorallocate($image, $config['bg_color'][0], $config['bg_color'][1], $config['bg_color'][2]); -$borderColor = imagecolorallocate($image, $config['border_color'][0], $config['border_color'][1], $config['border_color'][2]); -$textColor = imagecolorallocate($image, $config['text_color'][0], $config['text_color'][1], $config['text_color'][2]); - -// Fill background -imagefill($image, 0, 0, $bgColor); - -// Draw rounded rectangle (if rounded corners are requested) -if ($config['rounded'] > 0) { - // Draw filled rectangle - imagefilledrectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $bgColor); - - // Draw rounded corners - basic simulation for rounded corners - // This could be improved with proper arc drawing - $r = $config['rounded']; - - // Top left corner - imagefilledarc($image, $r, $r, $r * 2, $r * 2, 180, 270, $bgColor, IMG_ARC_PIE); - - // Top right corner - imagefilledarc($image, $imgWidth - $r - 1, $r, $r * 2, $r * 2, 270, 360, $bgColor, IMG_ARC_PIE); - - // Bottom left corner - imagefilledarc($image, $r, $imgHeight - $r - 1, $r * 2, $r * 2, 90, 180, $bgColor, IMG_ARC_PIE); - - // Bottom right corner - imagefilledarc($image, $imgWidth - $r - 1, $imgHeight - $r - 1, $r * 2, $r * 2, 0, 90, $bgColor, IMG_ARC_PIE); - - // Draw border - imagerectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor); -} else { - // Draw simple rectangle - imagefilledrectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $bgColor); - imagerectangle($image, 0, 0, $imgWidth - 1, $imgHeight - 1, $borderColor); -} - // Draw text -$y = $config['padding']; +$y = $textAreaY + $config['font_size']; // Start position for text foreach ($lines as $line) { if (isset($config['use_builtin_font'])) { // Use built-in font (less nice but always available) - imagestring($image, 5, $config['padding'], $y, $line, $textColor); + imagestring($canvas, 5, $textAreaX, $y, $line, $textColor); $y += imagefontheight(5); } else { // Use TrueType font (nicer but requires font file) - imagettftext($image, $config['font_size'], 0, $config['padding'], $y + $config['font_size'], $textColor, $config['font'], $line); - $y += $lineHeight; + imagettftext($canvas, $config['font_size'], 0, $textAreaX, $y, $textColor, $config['font'], $line); + $y += $config['line_height']; } } // Output the image -imagepng($image); +imagepng($canvas); // Clean up -imagedestroy($image); -imagedestroy($tempImage); \ No newline at end of file +imagedestroy($canvas); \ No newline at end of file