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; } }