77da0c5517
- Remove @getimagesize() suppression in ImageHelper, ImageGenerator, MokoOG — let PHP report warnings for corrupt/unreadable images - Add Log::add() when ImageHelper::resize() cannot read image dimensions - Check Folder::create() return value in ImageGenerator and ImageHelper, return graceful fallback if directory creation fails
183 lines
6.0 KiB
PHP
183 lines
6.0 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @package MokoSuiteOpenGraph
|
|
* @subpackage plg_system_mokoog
|
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
* @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);
|
|
}
|
|
}
|