<?php
|
namespace JVBase\managers;
|
|
use JVBase\managers\UploadManager;
|
use WP_Error;
|
use Imagick;
|
use ImagickPixel;
|
use Exception;
|
use ImagickDrawException;
|
use ImagickPixelException;
|
use ImagickDraw;
|
use ImagickException;
|
use GdImage;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
/**
|
* ImageGenerator
|
*
|
* Generates artist feature cards as webp images with artist information
|
* Uses Imagick with GD fallback and leverages FileUploadManager storage
|
*/
|
class ImageGenerator
|
{
|
// Configuration options
|
protected array $config = [
|
'width' => 2400,
|
'height' => 1260,
|
'padding' => 80,
|
'portrait_width' => 1200,
|
'text_area_width' => 800,
|
'background_color' => '#151515',
|
'text_color' => '#F9F9F9',
|
'accent_color' => '#FF0080',
|
'fonts' => [
|
'title' => [
|
'file' => 'courier-prime-v9-latin-regular.ttf',
|
'size' => 28,
|
],
|
'name' => [
|
'file' => 'mitr-v11-latin-700.ttf',
|
'size' => 100,
|
],
|
'info' => [
|
'file' => 'courier-prime-v9-latin-regular.ttf',
|
'size' => 24,
|
],
|
],
|
'output_quality' => 95,
|
'use_imagick' => null, // Will be set in constructor
|
];
|
|
// Instance variables
|
protected array $data;
|
protected string $fontsDir;
|
|
protected UploadManager|null $fileManager;
|
protected string $outputDir;
|
protected string $outputFilename;
|
protected int $textTop;
|
protected string $type;
|
|
/**
|
* Constructor
|
*/
|
/**
|
* @param array $data
|
* @param UploadManager|null $fileManager
|
*/
|
public function __construct(array $data, UploadManager|null $fileManager = null)
|
{
|
$this->data = $data;
|
|
$this->type = $data['imageType'];
|
|
// Set paths
|
$this->fontsDir = JVB_DIR. '/assets/fonts/';
|
|
// Check for Imagick support
|
$this->config['use_imagick'] = extension_loaded('imagick');
|
$this->textTop = (int)$this->config['height'] / 2;
|
|
// Set file manager if provided
|
$this->fileManager = $fileManager;
|
|
// Set default output directory if file manager not provided
|
if (!$this->fileManager) {
|
$this->outputDir = wp_upload_dir()['basedir'] . '/artists/';
|
if (!is_dir($this->outputDir)) {
|
wp_mkdir_p($this->outputDir);
|
}
|
}
|
}
|
|
/**
|
* Generate the artist card
|
*/
|
/**
|
* @return array
|
*/
|
public function generate():array
|
{
|
// Generate appropriate filename
|
$this->generateFilename();
|
|
// Generate image using appropriate method
|
$result = $this->config['use_imagick'] ?
|
$this->imagickGenerator() :
|
$this->GDGenerator();
|
|
if (!$result['success']) {
|
return $result;
|
}
|
|
// Create WordPress attachment
|
$attachment_id = $this->createAttachment($result['path']);
|
|
if (is_wp_error($attachment_id)) {
|
return [
|
'success' => false,
|
'error' => $attachment_id->get_error_message()
|
];
|
}
|
|
return [
|
'success' => true,
|
'attachment_id' => $attachment_id,
|
'url' => wp_get_attachment_url($attachment_id),
|
'path' => $result['path']
|
];
|
}
|
|
/**
|
* Create WordPress attachment from generated image
|
* @param string $file_path
|
*
|
* @return int|WP_Error
|
*/
|
protected function createAttachment(string $file_path):int|WP_Error
|
{
|
// Get upload directory based on user/content structure
|
$upload_dir = wp_upload_dir();
|
$base_dir = $upload_dir['basedir'];
|
|
// Get directory structure from FileUploadManager if available
|
if ($this->fileManager) {
|
$user_dir = $this->fileManager->getUploadDirectory();
|
$relative_dir = str_replace($base_dir, '', dirname($file_path));
|
$destination_dir = $base_dir . '/' . $relative_dir;
|
} else {
|
// Fallback to default user/artist structure
|
$user_data = get_userdata($this->data['user_id']);
|
switch ($this->type) {
|
case 'shop':
|
$root = 'shops';
|
$ID = $this->data['shop'];
|
break;
|
default:
|
$root = 'artists';
|
$ID = $this->data['user_id'];
|
break;
|
}
|
$destination_dir = $base_dir . '/'.$root.'/' . sanitize_file_name($ID);
|
}
|
|
// Create directory structure if it doesn't exist
|
if (!is_dir($destination_dir)) {
|
wp_mkdir_p($destination_dir);
|
}
|
|
// Move file to final location
|
$filename = basename($file_path);
|
$new_file = $destination_dir . '/' . $filename;
|
|
if (@copy($file_path, $new_file)) {
|
@unlink($file_path); // Delete original
|
$file_path = $new_file; // Update path reference
|
}
|
|
// Prepare attachment data
|
$artist_name = $this->data['display_name'];
|
$city = $this->data['city'];
|
|
switch ($this->type) {
|
case 'shop':
|
$title = 'Featured Shop Image';
|
break;
|
default:
|
$title = 'Featured Artist Image';
|
}
|
|
$attachment = [
|
'post_mime_type' => 'image/webp',
|
'post_title' => sprintf('%s - %s - %s', $artist_name, $title, $city),
|
'post_content' => '',
|
'post_status' => 'inherit',
|
'guid' => str_replace($base_dir, $upload_dir['baseurl'], $file_path)
|
];
|
|
// Insert attachment
|
$attachment_id = wp_insert_attachment($attachment, $file_path);
|
|
if (is_wp_error($attachment_id)) {
|
return $attachment_id;
|
}
|
|
// Make sure we have media functions
|
require_once(ABSPATH . 'wp-admin/includes/image.php');
|
require_once(ABSPATH . 'wp-admin/includes/file.php');
|
require_once(ABSPATH . 'wp-admin/includes/media.php');
|
|
|
switch ($this->type) {
|
case 'shop':
|
$type = 'Tattoo Studio';
|
break;
|
default:
|
$type = $this->data['type'];
|
break;
|
}
|
// Set alt text
|
$alt_text = sprintf(
|
'Featured image for %s - %sin %s',
|
$artist_name,
|
$type,
|
$city
|
);
|
update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text);
|
|
return $attachment_id;
|
}
|
|
/**
|
* Generate filename using FileUploadManager pattern or a fallback
|
* @return void
|
*/
|
protected function generateFilename():void
|
{
|
if ($this->fileManager) {
|
switch ($this->type) {
|
case 'shop':
|
$title = 'tattoo-shop';
|
break;
|
default:
|
$title = 'artist';
|
break;
|
}
|
// Use FileUploadManager to generate a filename
|
|
$user_data = get_userdata($this->data['user_id']);
|
$this->outputFilename = $this->fileManager->generateFilename(
|
$title.'.webp',
|
$user_data
|
);
|
} else {
|
// Fallback filename generation
|
$safeCity = sanitize_title($this->data['city']);
|
$timestamp = date('YmdHis');
|
|
switch ($this->type) {
|
case 'shop':
|
$safeName = sanitize_title($this->data['name']);
|
$base = 'shop';
|
break;
|
default:
|
$safeName = sanitize_title($this->data['display_name']);
|
$base = 'artist';
|
break;
|
}
|
$this->outputFilename = "{$safeCity}-{$base}-{$safeName}-{$timestamp}.webp";
|
}
|
}
|
|
/**
|
* Generate artist card using Imagick
|
* @return array
|
*/
|
protected function imagickGenerator():array
|
{
|
if ($this->type === 'shop') {
|
return $this->generateShopImagick();
|
}
|
try {
|
// Create a new image with background color
|
$card = new Imagick();
|
$card->newImage(
|
$this->config['width'],
|
$this->config['height'],
|
new ImagickPixel($this->config['background_color'])
|
);
|
$card->setImageFormat('webp');
|
|
// Load portrait image
|
$portrait = new Imagick(wp_get_attachment_image_src($this->data['image'], 'full')[0]);
|
|
// Resize portrait to fit the left side
|
$portrait->resizeImage(
|
$this->config['portrait_width'],
|
$this->config['height'],
|
Imagick::FILTER_LANCZOS,
|
1,
|
true
|
);
|
|
// Apply sepia and grayscale effects
|
$portrait->modulateImage(100, 60, 100); // Keep 60% of saturation (40% grayscale)
|
$portrait->sepiaToneImage(60); // 60% sepia
|
|
// Composite portrait onto card
|
$card->compositeImage(
|
$portrait,
|
Imagick::COMPOSITE_COPY,
|
0,
|
0
|
);
|
|
// Free portrait memory
|
$portrait->clear();
|
|
// Text positioning
|
$textX = $this->config['portrait_width'] + $this->config['padding'];
|
$textY = $this->config['padding'] * 2;
|
|
// Add "EDMONTON'S BEST TATTOO ARTISTS:" text
|
$title = "{$this->data['city']}'S BEST {$this->data['type']}:";
|
$this->drawImagickText(
|
$card,
|
$title,
|
$textX,
|
$textY,
|
$this->config['fonts']['title']['file'],
|
$this->config['fonts']['title']['size']
|
);
|
|
// Add artist name
|
$textY += $this->config['fonts']['title']['size'] + 30;
|
$this->drawImagickText(
|
$card,
|
strtoupper($this->data['display_name']),
|
$textX,
|
$textY,
|
$this->config['fonts']['name']['file'],
|
$this->config['fonts']['name']['size']
|
);
|
|
$icons = $this->getIcons();
|
|
// Add shop info with icon
|
$textY += $this->config['fonts']['name']['size'] + 40;
|
|
foreach ($icons as $i) {
|
$icon = new Imagick(JVB_DIR.'/assets/icons/'.$i['icon'].'.png');
|
$icon->resizeImage(36, 36, Imagick::FILTER_LANCZOS, 1);
|
|
$card->compositeImage($icon, Imagick::COMPOSITE_OVER, $textX, $textY - 15);
|
$this->drawImagickText(
|
$card,
|
" " . trim($i['name']),
|
$textX + 25,
|
$textY,
|
$this->config['fonts']['info']['file'],
|
$this->config['fonts']['info']['size']
|
);
|
$textY += $i['textY'];
|
}
|
|
// Set webp quality
|
$card->setImageCompressionQuality($this->config['output_quality']);
|
|
// Save the image
|
if ($this->fileManager) {
|
$outputPath = $this->fileManager->getUploadDirectory(
|
$this->data['user_id']
|
) . '/' . $this->outputFilename;
|
} else {
|
$outputPath = $this->outputDir . $this->outputFilename;
|
}
|
|
// Make sure the filename ends with .webp
|
if (!str_ends_with(strtolower($outputPath), '.webp')) {
|
$outputPath .= '.webp';
|
}
|
$card->writeImage($outputPath);
|
|
// Clean up
|
$card->clear();
|
|
return [
|
'success' => true,
|
'path' => $outputPath,
|
'url' => str_replace(wp_upload_dir()['basedir'], wp_upload_dir()['baseurl'], $outputPath),
|
'filename' => $this->outputFilename
|
];
|
} catch (Exception $e) {
|
return [
|
'success' => false,
|
'error' => $e->getMessage()
|
];
|
}
|
}
|
|
/**
|
* Draw text on Imagick image
|
* @param Imagick $image
|
* @param string $text
|
* @param int $x
|
* @param int $y
|
* @param string $fontFile
|
* @param int $fontSize
|
*
|
* @return void
|
* @throws ImagickDrawException
|
* @throws ImagickException
|
* @throws ImagickPixelException
|
*/
|
protected function drawImagickText(
|
Imagick $image,
|
string $text,
|
int $x,
|
int $y,
|
string $fontFile,
|
int $fontSize
|
):void {
|
$draw = new ImagickDraw();
|
$draw->setFont($this->fontsDir . $fontFile);
|
$draw->setFontSize($fontSize);
|
$draw->setFillColor(new ImagickPixel($this->config['text_color']));
|
$draw->setTextAntialias(true);
|
|
$image->annotateImage($draw, $x, $y, 0, $text);
|
}
|
|
/**
|
* Generate logo preview with Imagick
|
* @return array
|
*/
|
protected function generateShopImagick():array
|
{
|
try {
|
// Create a new square image with background
|
$width = 1200;
|
$height = 630;
|
$card = new Imagick();
|
$card->newImage(
|
$width,
|
$height,
|
new ImagickPixel($this->config['background_color'])
|
);
|
$card->setImageFormat('webp');
|
|
// Load logo image
|
$logo = new Imagick(wp_get_attachment_image_src($this->data['image'], 'full')[0]);
|
|
// Get dominant color from logo for background gradient
|
$dominantColor = $this->getDominantColorImagick($logo);
|
|
// Create gradient background
|
$this->createGradientBackgroundImagick($card, $dominantColor);
|
|
// Resize logo to fit in center (max 70% of width/height)
|
$logoWidth = $logo->getImageWidth();
|
$logoHeight = $logo->getImageHeight();
|
$maxLogoWidth = $width * 0.7;
|
$maxLogoHeight = $height * 0.7;
|
|
// Calculate scaling factor to fit within bounds
|
$scale = min($maxLogoWidth / $logoWidth, $maxLogoHeight / $logoHeight);
|
|
// Resize logo
|
$logo->resizeImage(
|
intval($logoWidth * $scale),
|
intval($logoHeight * $scale),
|
Imagick::FILTER_LANCZOS,
|
1
|
);
|
|
// Center logo on canvas
|
$x = ($width - $logo->getImageWidth()) / 2;
|
$y = ($height - $logo->getImageHeight()) / 2;
|
|
// Composite logo onto card
|
$card->compositeImage(
|
$logo,
|
Imagick::COMPOSITE_OVER,
|
intval($x),
|
intval($y)
|
);
|
|
// Free logo memory
|
$logo->clear();
|
|
// Set webp quality
|
$card->setImageCompressionQuality($this->config['output_quality']);
|
|
// Save the image
|
if ($this->fileManager) {
|
$outputPath = $this->fileManager->getUploadDirectory(
|
$this->data['user_id']
|
) . '/' . $this->outputFilename;
|
} else {
|
$outputPath = $this->outputDir . $this->outputFilename;
|
}
|
|
// Make sure the filename ends with .webp
|
if (!str_ends_with(strtolower($outputPath), '.webp')) {
|
$outputPath .= '.webp';
|
}
|
$card->writeImage($outputPath);
|
|
// Clean up
|
$card->clear();
|
|
return [
|
'success' => true,
|
'path' => $outputPath,
|
'url' => str_replace(wp_upload_dir()['basedir'], wp_upload_dir()['baseurl'], $outputPath),
|
'filename' => $this->outputFilename
|
];
|
} catch (Exception $e) {
|
return [
|
'success' => false,
|
'error' => $e->getMessage()
|
];
|
}
|
}
|
|
/**
|
* Extract dominant color from image using Imagick
|
* @param Imagick $image
|
* @return string Hex color
|
*/
|
protected function getDominantColorImagick(Imagick $image): string
|
{
|
try {
|
// Create a copy for analysis
|
$copy = clone $image;
|
|
// Resize to a small image for faster processing
|
$copy->resizeImage(50, 50, Imagick::FILTER_LANCZOS, 1);
|
|
// Reduce to one pixel to get average color
|
$copy->resizeImage(1, 1, Imagick::FILTER_LANCZOS, 1);
|
|
// Get the color of that pixel
|
$pixel = $copy->getImagePixelColor(0, 0);
|
$colorArray = $pixel->getColor();
|
|
// Convert to hex
|
$hexColor = sprintf(
|
"#%02x%02x%02x",
|
$colorArray['r'],
|
$colorArray['g'],
|
$colorArray['b']
|
);
|
|
$copy->clear();
|
|
// Ensure we have sufficient color intensity
|
$brightness = ($colorArray['r'] + $colorArray['g'] + $colorArray['b']) / 3;
|
if ($brightness < 30 || $brightness > 225) {
|
// If too dark or too light, use accent color instead
|
return $this->config['accent_color'];
|
}
|
|
return $hexColor;
|
} catch (Exception $e) {
|
// On error, return default accent color
|
return $this->config['accent_color'];
|
}
|
}
|
|
/**
|
* Create gradient background with Imagick
|
* @param Imagick $canvas
|
* @param string $color Center color
|
* @return void
|
*/
|
protected function createGradientBackgroundImagick(Imagick &$canvas, string $color): void
|
{
|
$width = $canvas->getImageWidth();
|
$height = $canvas->getImageHeight();
|
$centerX = $width / 2;
|
$centerY = $height / 2;
|
$radius = max($width, $height) * 0.6; // Gradient radius
|
|
// Create radial gradient for background
|
$gradient = new Imagick();
|
$gradient->newPseudoImage(
|
$width,
|
$height,
|
"radial-gradient:$color-" . $this->config['background_color']
|
);
|
|
// Apply gradient to canvas
|
$canvas->compositeImage(
|
$gradient,
|
Imagick::COMPOSITE_OVER,
|
0,
|
0
|
);
|
|
$gradient->clear();
|
}
|
|
/**
|
* Generate artist card using GD
|
*/
|
/**
|
* @return array
|
* @throws Exception
|
*/
|
protected function GDGenerator():array
|
{
|
if ($this->type === 'shop') {
|
return $this->generateShopGD();
|
}
|
// Create a new image with background color
|
$card = imagecreatetruecolor($this->config['width'], $this->config['height']);
|
|
// Convert hex color to RGB
|
$bgColor = $this->hexToRgb($this->config['background_color']);
|
$textColor = $this->hexToRgb($this->config['text_color']);
|
|
// Allocate colors
|
$bgColorAllocated = imagecolorallocate($card, $bgColor['r'], $bgColor['g'], $bgColor['b']);
|
$textColorAllocated = imagecolorallocate($card, $textColor['r'], $textColor['g'], $textColor['b']);
|
|
// Fill background
|
imagefill($card, 0, 0, $bgColorAllocated);
|
|
// Load and resize portrait
|
$portrait = $this->loadAndResizeImage(
|
(int)$this->data['image'],
|
$this->config['portrait_width'],
|
$this->config['height']
|
);
|
|
// Apply sepia and grayscale effects to portrait
|
$this->applyGDSepiaTone($portrait);
|
|
// Copy portrait to card
|
imagecopy($card, $portrait, 0, 0, 0, 0, imagesx($portrait), imagesy($portrait));
|
|
// Free portrait memory
|
imagedestroy($portrait);
|
|
// Text positioning
|
$textX = $this->config['portrait_width'] + $this->config['padding'];
|
|
// Text positioning constants
|
$headerY = $this->textTop; // "EDMONTON'S BEST TATTOO ARTIST:"
|
$nameY = $this->textTop + 135; // Artist name
|
$detailsStartY = $this->textTop + 160; // Start Y for shop/location/styles
|
$detailsLineHeight = 45; // Space between detail lines
|
|
// Add title with courier font
|
$this->drawGDText(
|
$card,
|
strtoupper("{$this->data['city']}'S BEST TATTOO ARTISTS:"),
|
$textX,
|
$headerY,
|
$this->config['fonts']['title']['file'],
|
$this->config['fonts']['title']['size'],
|
$textColorAllocated
|
);
|
|
// Add artist name in larger, bolder font
|
$this->drawGDText(
|
$card,
|
strtoupper($this->data['display_name']),
|
$textX,
|
$nameY,
|
$this->config['fonts']['name']['file'],
|
$this->config['fonts']['name']['size'],
|
$textColorAllocated
|
);
|
|
// Current Y position for details
|
$currentY = $detailsStartY + 50;
|
|
// Add shop info with icon
|
$shopIcon = $this->loadAndResizeImage(JVB_DIR.'/assets/icons/shop.png', 36, 36);
|
$this->copyImageWithTransparency($card, $shopIcon, $textX, $currentY - 27);
|
$this->drawGDText(
|
$card,
|
" {$this->data['shop']}",
|
$textX + 15,
|
$currentY,
|
$this->config['fonts']['info']['file'],
|
$this->config['fonts']['info']['size'],
|
$textColorAllocated
|
);
|
|
$currentY += $detailsLineHeight;
|
|
// Add location with icon
|
$locationIcon = $this->loadAndResizeImage(JVB_DIR.'/assets/icons/city.png', 36, 36);
|
$this->copyImageWithTransparency($card, $locationIcon, $textX, $currentY - 25);
|
$this->drawGDText(
|
$card,
|
" {$this->data['city']}",
|
$textX + 15,
|
$currentY,
|
$this->config['fonts']['info']['file'],
|
$this->config['fonts']['info']['size'],
|
$textColorAllocated
|
);
|
$currentY += $detailsLineHeight;
|
|
// Add styles with hashtags
|
$styles = explode(',', strtolower($this->data['styles']));
|
$styleIcon = $this->loadAndResizeImage(JVB_DIR.'/assets/icons/style.png', 36, 36);
|
foreach ($styles as $style) {
|
$this->copyImageWithTransparency($card, $styleIcon, $textX, $currentY - 25);
|
$this->drawGDText(
|
$card,
|
" {$style}",
|
$textX + 15,
|
$currentY,
|
$this->config['fonts']['info']['file'],
|
$this->config['fonts']['info']['size'],
|
$textColorAllocated
|
);
|
$currentY += $detailsLineHeight;
|
}
|
|
// Save the image
|
if ($this->fileManager) {
|
$outputPath = $this->fileManager->getUploadDirectory($this->data['user_id']) . '/' . $this->outputFilename;
|
} else {
|
$outputPath = $this->outputDir . $this->outputFilename;
|
}
|
|
// Make sure the filename ends with .webp
|
if (!str_ends_with(strtolower($outputPath), '.webp')) {
|
$outputPath .= '.webp';
|
}
|
|
// Make sure the directory exists
|
wp_mkdir_p(dirname($outputPath));
|
|
// Create WebP
|
imagewebp($card, $outputPath, $this->config['output_quality']);
|
|
// Clean up
|
imagedestroy($card);
|
|
return [
|
'success' => true,
|
'path' => $outputPath,
|
'url' => str_replace(wp_upload_dir()['basedir'], wp_upload_dir()['baseurl'], $outputPath),
|
'filename' => $this->outputFilename
|
];
|
}
|
|
/**
|
* Draw text on GD image
|
* @param GdImage $image
|
* @param string $text
|
* @param int $x
|
* @param int $y
|
* @param string $fontFile
|
* @param int $fontSize
|
* @param mixed $color
|
*
|
* @return void
|
*/
|
protected function drawGDText(
|
GdImage $image,
|
string $text,
|
int $x,
|
int $y,
|
string $fontFile,
|
int $fontSize,
|
mixed $color
|
):void {
|
imagettftext(
|
$image,
|
$fontSize,
|
0,
|
$x,
|
$y,
|
$color,
|
$this->fontsDir . $fontFile,
|
$text
|
);
|
}
|
|
/**
|
* Generate logo preview with GD
|
* @return array
|
*/
|
protected function generateShopGD():array
|
{
|
try {
|
// Create dimensions
|
$width = 1200;
|
$height = 630;
|
|
// Create a new image
|
$card = imagecreatetruecolor($width, $height);
|
|
// Convert hex color to RGB
|
$bgColor = $this->hexToRgb($this->config['background_color']);
|
|
// Allocate background color
|
$bgColorAllocated = imagecolorallocate($card, $bgColor['r'], $bgColor['g'], $bgColor['b']);
|
|
// Fill background
|
imagefill($card, 0, 0, $bgColorAllocated);
|
|
// Load logo
|
$logoImage = $this->loadAndResizeImage(
|
(int)$this->data['image'],
|
$width,
|
$height
|
);
|
|
// Get dominant color from logo
|
$dominantColor = $this->getDominantColorGD($logoImage);
|
|
// Create gradient background
|
$this->createGradientBackgroundGD($card, $dominantColor);
|
|
// Calculate new dimensions (max 70% of width/height)
|
$logoWidth = imagesx($logoImage);
|
$logoHeight = imagesy($logoImage);
|
$maxLogoWidth = $width * 0.7;
|
$maxLogoHeight = $height * 0.7;
|
|
// Calculate scaling factor
|
$scale = min($maxLogoWidth / $logoWidth, $maxLogoHeight / $logoHeight);
|
|
// Create resized logo
|
$resizedLogo = imagecreatetruecolor(
|
intval($logoWidth * $scale),
|
intval($logoHeight * $scale)
|
);
|
|
// Handle transparency
|
imagealphablending($resizedLogo, false);
|
imagesavealpha($resizedLogo, true);
|
$transparent = imagecolorallocatealpha($resizedLogo, 255, 255, 255, 127);
|
imagefilledrectangle($resizedLogo, 0, 0, $logoWidth * $scale, $logoHeight * $scale, $transparent);
|
|
// Resize logo
|
imagecopyresampled(
|
$resizedLogo,
|
$logoImage,
|
0,
|
0,
|
0,
|
0,
|
intval($logoWidth * $scale),
|
intval($logoHeight * $scale),
|
$logoWidth,
|
$logoHeight
|
);
|
|
// Calculate position to center the logo
|
$x = intval(($width - imagesx($resizedLogo)) / 2);
|
$y = intval(($height - imagesy($resizedLogo)) / 2);
|
|
// Copy logo to card with transparency
|
imagealphablending($card, true);
|
imagecopy(
|
$card,
|
$resizedLogo,
|
$x,
|
$y,
|
0,
|
0,
|
imagesx($resizedLogo),
|
imagesy($resizedLogo)
|
);
|
|
// Clean up
|
imagedestroy($logoImage);
|
imagedestroy($resizedLogo);
|
|
// Save the image
|
if ($this->fileManager) {
|
$outputPath = $this->fileManager->getUploadDirectory(
|
$this->data['user_id']
|
) .'/' . $this->outputFilename;
|
} else {
|
$outputPath = $this->outputDir . $this->outputFilename;
|
}
|
|
// Make sure the filename ends with .webp
|
if (!str_ends_with(strtolower($outputPath), '.webp')) {
|
$outputPath .= '.webp';
|
}
|
|
// Make sure the directory exists
|
wp_mkdir_p(dirname($outputPath));
|
|
// Create WebP
|
imagewebp($card, $outputPath, $this->config['output_quality']);
|
|
// Clean up
|
imagedestroy($card);
|
|
return [
|
'success' => true,
|
'path' => $outputPath,
|
'url' => str_replace(wp_upload_dir()['basedir'], wp_upload_dir()['baseurl'], $outputPath),
|
'filename' => $this->outputFilename
|
];
|
} catch (Exception $e) {
|
return [
|
'success' => false,
|
'error' => $e->getMessage()
|
];
|
}
|
}
|
|
/**
|
* Extract dominant color from image using GD
|
* @param GdImage $image
|
* @return string Hex color
|
*/
|
protected function getDominantColorGD(GdImage $image): string
|
{
|
// Resize image to 1x1 to get average color
|
$tmp = imagecreatetruecolor(1, 1);
|
imagecopyresampled($tmp, $image, 0, 0, 0, 0, 1, 1, imagesx($image), imagesy($image));
|
|
// Get the color index of the pixel
|
$rgb = imagecolorat($tmp, 0, 0);
|
|
// Extract the RGB values
|
$r = ($rgb >> 16) & 0xFF;
|
$g = ($rgb >> 8) & 0xFF;
|
$b = $rgb & 0xFF;
|
|
// Clean up
|
imagedestroy($tmp);
|
|
// Generate hex color
|
$hexColor = sprintf("#%02x%02x%02x", $r, $g, $b);
|
|
// Check if the color is too dark or too light
|
$brightness = ($r + $g + $b) / 3;
|
if ($brightness < 30 || $brightness > 225) {
|
// If too dark or too light, use accent color
|
return $this->config['accent_color'];
|
}
|
|
return $hexColor;
|
}
|
|
/**
|
* Create gradient background with GD
|
* @param GdImage $canvas
|
* @param string $color Center color
|
* @return void
|
*/
|
protected function createGradientBackgroundGD(GdImage &$canvas, string $color):void
|
{
|
$width = imagesx($canvas);
|
$height = imagesy($canvas);
|
$centerX = $width / 2;
|
$centerY = $height / 2;
|
|
// Convert hex to RGB
|
$centerColor = $this->hexToRgb($color);
|
$edgeColor = $this->hexToRgb($this->config['background_color']);
|
|
// Create gradient
|
$maxDist = sqrt(pow($width / 2, 2) + pow($height / 2, 2));
|
|
for ($y = 0; $y < $height; $y++) {
|
for ($x = 0; $x < $width; $x++) {
|
// Calculate distance from center (0-1 range)
|
$distX = abs($x - $centerX);
|
$distY = abs($y - $centerY);
|
$dist = sqrt(pow($distX, 2) + pow($distY, 2)) / $maxDist;
|
|
// Adjust for more center focus
|
$dist = pow($dist, 0.8);
|
|
// Blend colors based on distance
|
$r = intval($centerColor['r'] * (1 - $dist) + $edgeColor['r'] * $dist);
|
$g = intval($centerColor['g'] * (1 - $dist) + $edgeColor['g'] * $dist);
|
$b = intval($centerColor['b'] * (1 - $dist) + $edgeColor['b'] * $dist);
|
|
// Set pixel
|
$pixelColor = imagecolorallocate($canvas, $r, $g, $b);
|
imagesetpixel($canvas, $x, $y, $pixelColor);
|
}
|
}
|
}
|
|
/**
|
* Load and resize an image with GD
|
* @param string|int $imagePath
|
* @param int $width
|
* @param int $height
|
*
|
* @return false|GdImage
|
* @throws Exception
|
*/
|
protected function loadAndResizeImage(string|int $imagePath, int $width, int $height):GdImage|false
|
{
|
// Get image info
|
if (is_int($imagePath)) {
|
$imagePath = get_attached_file($imagePath);
|
}
|
|
if (empty($imagePath)) {
|
return false;
|
}
|
$imageInfo = getimagesize($imagePath);
|
|
// Create image from source based on file type
|
switch ($imageInfo[2]) {
|
case IMAGETYPE_JPEG:
|
$sourceImage = imagecreatefromjpeg($imagePath);
|
break;
|
case IMAGETYPE_PNG:
|
$sourceImage = imagecreatefrompng($imagePath);
|
break;
|
case IMAGETYPE_GIF:
|
$sourceImage = imagecreatefromgif($imagePath);
|
break;
|
case IMAGETYPE_WEBP:
|
$sourceImage = imagecreatefromwebp($imagePath);
|
break;
|
default:
|
throw new Exception("Unsupported image type");
|
}
|
|
// Calculate dimensions while maintaining aspect ratio
|
$sourceWidth = imagesx($sourceImage);
|
$sourceHeight = imagesy($sourceImage);
|
|
$sourceRatio = $sourceWidth / $sourceHeight;
|
$targetRatio = $width / $height;
|
|
if ($sourceRatio > $targetRatio) {
|
// Image is wider than target, adjust width
|
$newWidth = $height * $sourceRatio;
|
$newHeight = $height;
|
} else {
|
// Image is taller than target, adjust height
|
$newWidth = $width;
|
$newHeight = $width / $sourceRatio;
|
}
|
|
// Create resized image
|
$resizedImage = imagecreatetruecolor($width, $height);
|
|
// Preserve transparency for PNG
|
if ($imageInfo[2] === IMAGETYPE_PNG) {
|
imagealphablending($resizedImage, false);
|
imagesavealpha($resizedImage, true);
|
$transparent = imagecolorallocatealpha($resizedImage, 255, 255, 255, 127);
|
imagefilledrectangle($resizedImage, 0, 0, $width, $height, $transparent);
|
}
|
|
// Resize and crop to the center
|
$srcX = (int)($sourceWidth - $width * ($sourceHeight / $height)) / 2;
|
$srcY = (int)($sourceHeight - $height * ($sourceWidth / $width)) / 2;
|
|
$srcX = ($srcX <0) ? 0 : $srcX;
|
$srcY = ($srcY <0) ? 0 : $srcY;
|
|
// Calculate source dimensions
|
$srcWidth = (int)($sourceWidth - (2 * $srcX));
|
$srcHeight = (int)($sourceHeight - (2 * $srcY));
|
|
imagecopyresampled(
|
$resizedImage,
|
$sourceImage,
|
0,
|
0,
|
$srcX,
|
$srcY,
|
$width,
|
$height,
|
$srcWidth,
|
$srcHeight
|
);
|
|
// Free original image memory
|
imagedestroy($sourceImage);
|
|
return $resizedImage;
|
}
|
|
/**
|
* Apply sepia and grayscale effect to GD image
|
* @param GdImage $image
|
*
|
* @return void
|
*/
|
protected function applyGDSepiaTone(GdImage $image):void
|
{
|
// First apply grayscale at 40%
|
// To do this, we'll create a copy of the image, grayscale it completely,
|
// then merge back at 40% opacity
|
$width = imagesx($image);
|
$height = imagesy($image);
|
|
// Create a grayscale copy
|
$gray = imagecreatetruecolor($width, $height);
|
imagecopy($gray, $image, 0, 0, 0, 0, $width, $height);
|
imagefilter($gray, IMG_FILTER_GRAYSCALE);
|
|
// Merge back at 40%
|
$this->imageCopyMergeAlpha($image, $gray, 0, 0, 0, 0, $width, $height, 40);
|
imagedestroy($gray);
|
|
// Now apply sepia at 60%
|
// Sepia is essentially a color shift towards brown/yellow
|
// We'll use colorize to achieve this, but at 60% strength
|
imagefilter($image, IMG_FILTER_COLORIZE, 112, 66, 20, 60);
|
}
|
|
/**
|
* @param GdImage $dst_im
|
* @param GdImage $src_im
|
* @param int $dst_x
|
* @param int $dst_y
|
* @param int $src_x
|
* @param int $src_y
|
* @param int $src_w
|
* @param int $src_h
|
* @param int $pct
|
*
|
* @return void
|
*/
|
protected function imageCopyMergeAlpha(
|
GdImage $dst_im,
|
GdImage $src_im,
|
int $dst_x,
|
int $dst_y,
|
int $src_x,
|
int $src_y,
|
int $src_w,
|
int $src_h,
|
int $pct
|
):void {
|
// Creating a cut resource
|
$cut = imagecreatetruecolor($src_w, $src_h);
|
|
// Copying relevant section from destination image
|
imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h);
|
|
// Copying relevant section from source image
|
imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h);
|
|
// Insert merged image at destination
|
imagecopymerge($dst_im, $cut, $dst_x, $dst_y, 0, 0, $src_w, $src_h, $pct);
|
|
imagedestroy($cut);
|
}
|
|
/**
|
* Copy an image while preserving transparency
|
* @param GdImage $destination
|
* @param GdImage $source
|
* @param int $destX
|
* @param int $destY
|
*
|
* @return void
|
*/
|
protected function copyImageWithTransparency(GdImage $destination, GdImage $source, int $destX, int $destY):void
|
{
|
$width = imagesx($source);
|
$height = imagesy($source);
|
|
imagecopy($destination, $source, $destX, $destY, 0, 0, $width, $height);
|
|
// Free source memory
|
imagedestroy($source);
|
}
|
|
/**
|
* Convert hex color to RGB array
|
* @param string $hexColor
|
*
|
* @return array
|
*/
|
protected function hexToRgb(string $hexColor):array
|
{
|
$hex = ltrim($hexColor, '#');
|
|
return [
|
'r' => hexdec(substr($hex, 0, 2)),
|
'g' => hexdec(substr($hex, 2, 2)),
|
'b' => hexdec(substr($hex, 4, 2))
|
];
|
}
|
|
/**
|
* Gets an array of icon data to loop over
|
* @return array
|
*/
|
protected function getIcons():array
|
{
|
$icons = [
|
[
|
'textY' => 30,
|
'icon' => 'shop',
|
'name' => $this->data['shop']
|
],
|
[
|
'textY' => 40,
|
'icon' => 'city',
|
'name' => $this->data['city']
|
],
|
];
|
$styles = explode(',', $this->data['styles']);
|
foreach ($styles as $style) {
|
$icons[] = [
|
'textY' => 30,
|
'icon' => 'style',
|
'name' => $style
|
];
|
}
|
|
return $icons;
|
}
|
}
|