* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved. * @license GNU General Public License version 3 or later; see LICENSE */ namespace Joomla\Plugin\System\MokoOG\Helper; defined('_JEXEC') or die; use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Log\Log; class ImageGenerator { private const WIDTH = 1200; private const HEIGHT = 630; private const OUTPUT_DIR = 'images/mokoog/generated'; /** * Generate an OG image with title text overlaid on a template background. * * @param string $title Article title to overlay * @param string $templateImage Path to template/background image relative to JPATH_ROOT * @param string $fontFile Absolute path to TTF font file * @param int $fontSize Font size in points (default 42) * @param array $fontColor RGB array [r, g, b] (default white) * @param int $quality JPEG quality (default 90) * * @return string Path to generated image relative to JPATH_ROOT, or empty on failure */ public static function generate( string $title, string $templateImage, string $fontFile = '', int $fontSize = 42, array $fontColor = [255, 255, 255], int $quality = 90 ): string { if (!\extension_loaded('gd')) { Log::add('MokoOG ImageGenerator: GD extension is not loaded. Image generation disabled.', Log::WARNING, 'mokoog'); return ''; } $templateAbs = JPATH_ROOT . '/' . ltrim($templateImage, '/'); if (!is_file($templateAbs)) { Log::add('MokoOG ImageGenerator: Template image not found: ' . $templateImage, Log::WARNING, 'mokoog'); return ''; } if (!$fontFile || !is_file($fontFile)) { Log::add('MokoOG ImageGenerator: TTF font file not found: ' . ($fontFile ?: '(not configured)'), Log::WARNING, 'mokoog'); return ''; } $outputDir = JPATH_ROOT . '/' . self::OUTPUT_DIR; if (!is_dir($outputDir) && !Folder::create($outputDir)) { Log::add('MokoOG ImageGenerator: Cannot create output directory: ' . self::OUTPUT_DIR, Log::WARNING, 'mokoog'); return ''; } $hash = md5($title . $templateImage . $fontSize); $outputName = 'overlay_' . $hash . '.jpg'; $outputPath = $outputDir . '/' . $outputName; $outputRel = self::OUTPUT_DIR . '/' . $outputName; // Skip if already generated if (is_file($outputPath)) { return $outputRel; } // Load template image $imageInfo = getimagesize($templateAbs); if (!$imageInfo) { Log::add('MokoOG ImageGenerator: Cannot read image dimensions: ' . $templateImage, Log::WARNING, 'mokoog'); return ''; } $source = match ($imageInfo[2]) { IMAGETYPE_JPEG => imagecreatefromjpeg($templateAbs), IMAGETYPE_PNG => imagecreatefrompng($templateAbs), IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? imagecreatefromwebp($templateAbs) : false, default => false, }; if (!$source) { Log::add('MokoOG ImageGenerator: Failed to load image (unsupported type or corrupt): ' . $templateImage, Log::WARNING, 'mokoog'); return ''; } // Create output canvas at target dimensions $canvas = imagecreatetruecolor(self::WIDTH, self::HEIGHT); imagecopyresampled( $canvas, $source, 0, 0, 0, 0, self::WIDTH, self::HEIGHT, $imageInfo[0], $imageInfo[1] ); imagedestroy($source); // Semi-transparent overlay for text readability $overlay = imagecolorallocatealpha($canvas, 0, 0, 0, 64); imagefilledrectangle($canvas, 0, (int) (self::HEIGHT * 0.55), self::WIDTH, self::HEIGHT, $overlay); // Render title text with word wrapping $textColor = imagecolorallocate($canvas, $fontColor[0], $fontColor[1], $fontColor[2]); $wrappedTitle = self::wrapText($title, $fontFile, $fontSize, (int) (self::WIDTH * 0.85)); $textX = (int) (self::WIDTH * 0.075); $textY = (int) (self::HEIGHT * 0.72); imagettftext($canvas, $fontSize, 0, $textX, $textY, $textColor, $fontFile, $wrappedTitle); // Save imagejpeg($canvas, $outputPath, $quality); imagedestroy($canvas); return $outputRel; } /** * Wrap text to fit within a maximum pixel width. * * @param string $text Text to wrap * @param string $fontFile Path to TTF font * @param int $fontSize Font size in points * @param int $maxWidth Maximum width in pixels * * @return string Wrapped text with newlines */ private static function wrapText(string $text, string $fontFile, int $fontSize, int $maxWidth): string { $words = explode(' ', $text); $lines = []; $line = ''; foreach ($words as $word) { $testLine = $line ? $line . ' ' . $word : $word; $bbox = imagettfbbox($fontSize, 0, $fontFile, $testLine); $lineWidth = abs($bbox[4] - $bbox[0]); if ($lineWidth > $maxWidth && $line !== '') { $lines[] = $line; $line = $word; } else { $line = $testLine; } } if ($line !== '') { $lines[] = $line; } // Limit to 3 lines, truncate last line if needed if (\count($lines) > 3) { $lines = \array_slice($lines, 0, 3); if (mb_strlen($lines[2]) > 3) { $lines[2] = mb_substr($lines[2], 0, -3) . '...'; } else { $lines[2] .= '...'; } } return implode("\n", $lines); } }