- Add horizontal centering for each line of text - Add vertical centering for the entire text block - Ensure text doesn't overflow the top of the bubble - Maintain compatibility with both built-in and TTF fonts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
378 lines
15 KiB
PHP
378 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* SillyBubble Image Generator
|
|
*
|
|
* This script generates comic-style chat bubble images with characters.
|
|
* It supports character-based styling with speech or thought bubbles.
|
|
*
|
|
* Usage:
|
|
* image.php?q=Hello+World
|
|
* image.php?q=Hello+World&character=Example
|
|
* image.php?q=Hello+World&character=Example&bubble_type=thought
|
|
* image.php?q=Hello+World&style=Example (legacy style parameter maps to character)
|
|
*/
|
|
|
|
// Set content type to PNG image
|
|
header('Content-Type: image/png');
|
|
|
|
// Get parameters
|
|
$text = isset($_GET['q']) ? urldecode($_GET['q']) : 'No text provided';
|
|
$character = isset($_GET['character']) ? $_GET['character'] : 'Example';
|
|
$bubble_type = isset($_GET['bubble_type']) ? $_GET['bubble_type'] : 'speech';
|
|
$style = isset($_GET['style']) ? $_GET['style'] : 'default';
|
|
|
|
// If style parameter is used but no character is specified, use style as character name
|
|
if (!isset($_GET['character']) && isset($_GET['style']) && $style != 'default') {
|
|
$character = $style;
|
|
}
|
|
|
|
// Validate bubble_type - only 'speech' or 'thought' allowed
|
|
if ($bubble_type != 'speech' && $bubble_type != 'thought') {
|
|
$bubble_type = 'speech';
|
|
}
|
|
|
|
// Define canvas dimensions (5:1 ratio)
|
|
$canvasWidth = 2000;
|
|
$canvasHeight = 400;
|
|
$charWidth = 400; // 20% of canvas width
|
|
$bubbleWidth = 1400; // 70% of canvas width
|
|
$bubbleMargin = 100; // 5% margin on each side
|
|
$bubblePadding = 50; // Padding inside the bubble
|
|
|
|
// Define legacy styles for backward compatibility
|
|
$styles = [
|
|
'default' => [
|
|
'bg_color' => [245, 245, 245],
|
|
'text_color' => [50, 50, 50],
|
|
'border_color' => [200, 200, 200],
|
|
'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],
|
|
'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],
|
|
'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],
|
|
'font_size' => 24,
|
|
'line_height' => 30,
|
|
'font' => __DIR__ . '/fonts/NotoSans-Regular.ttf',
|
|
],
|
|
];
|
|
|
|
// 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',
|
|
];
|
|
|
|
// Ensure required directories exist
|
|
$dirs = [
|
|
__DIR__ . '/characters/Example',
|
|
__DIR__ . '/fonts'
|
|
];
|
|
|
|
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
|
|
$fontFiles = glob(__DIR__ . '/fonts/*.ttf');
|
|
if (!empty($fontFiles)) {
|
|
$config['font'] = $fontFiles[0];
|
|
} else {
|
|
$config['use_builtin_font'] = true;
|
|
}
|
|
}
|
|
|
|
// 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 = $textAreaWidth;
|
|
|
|
foreach ($words as $word) {
|
|
$testLine = $currentLine . ' ' . $word;
|
|
$testLine = ltrim($testLine); // Remove leading space
|
|
|
|
// Calculate width using built-in or TTF font
|
|
if (isset($config['use_builtin_font'])) {
|
|
$textWidth = imagefontwidth(5) * strlen($testLine);
|
|
} else {
|
|
$bbox = imagettfbbox($config['font_size'], 0, $config['font'], $testLine);
|
|
$textWidth = $bbox[2] - $bbox[0];
|
|
}
|
|
|
|
if ($textWidth > $maxWidth && $currentLine !== '') {
|
|
$lines[] = $currentLine;
|
|
$currentLine = $word;
|
|
} else {
|
|
$currentLine = $testLine;
|
|
}
|
|
}
|
|
if ($currentLine !== '') {
|
|
$lines[] = $currentLine;
|
|
}
|
|
|
|
// Center text vertically in the bubble
|
|
$lineCount = count($lines);
|
|
if (isset($config['use_builtin_font'])) {
|
|
$totalTextHeight = $lineCount * imagefontheight(5);
|
|
} else {
|
|
$totalTextHeight = $lineCount * $config['line_height'];
|
|
}
|
|
|
|
// Calculate vertical starting position for centered text
|
|
$startY = $textAreaY + ($textAreaHeight - $totalTextHeight) / 2;
|
|
if ($startY < $textAreaY) $startY = $textAreaY; // Ensure text doesn't overflow top
|
|
|
|
// Draw text
|
|
$y = $startY + $config['font_size']; // Start position for centered text
|
|
foreach ($lines as $line) {
|
|
if (isset($config['use_builtin_font'])) {
|
|
// Calculate width for centering horizontally
|
|
$lineWidth = imagefontwidth(5) * strlen($line);
|
|
$x = $textAreaX + ($textAreaWidth - $lineWidth) / 2;
|
|
|
|
// Use built-in font (less nice but always available)
|
|
imagestring($canvas, 5, $x, $y, $line, $textColor);
|
|
$y += imagefontheight(5);
|
|
} else {
|
|
// Calculate width for centering horizontally
|
|
$bbox = imagettfbbox($config['font_size'], 0, $config['font'], $line);
|
|
$lineWidth = $bbox[2] - $bbox[0];
|
|
$x = $textAreaX + ($textAreaWidth - $lineWidth) / 2;
|
|
|
|
// Use TrueType font (nicer but requires font file)
|
|
imagettftext($canvas, $config['font_size'], 0, $x, $y, $textColor, $config['font'], $line);
|
|
$y += $config['line_height'];
|
|
}
|
|
}
|
|
|
|
// Output the image
|
|
imagepng($canvas);
|
|
|
|
// Clean up
|
|
imagedestroy($canvas); |