// Couleurs logo : rouge, bleu fonce, orange $accents = [ imagecolorallocate($img, 230, 51, 41), imagecolorallocate($img, 22, 58, 130), imagecolorallocate($img, 245, 148, 29), ]; private int $logo_ai_id; private int $logo_banner_id; private string $upload_dir; private string $api_key; private string $provider; public function __construct() { $this->logo_ai_id = (int)get_option('aiag_logo_ai_id', 0); $this->logo_banner_id = (int)get_option('aiag_logo_banner_id', 0); $uploads = wp_upload_dir(); $this->upload_dir = $uploads['basedir'] . '/armenie-info-visuals/'; $this->api_key = (string)get_option('aiag_api_key', ''); $this->provider = (string)get_option('aiag_api_provider', 'claude'); if ( ! file_exists($this->upload_dir) ) wp_mkdir_p($this->upload_dir); } public function generate( array $article, string $photo_url = '' ): string { if ( ! extension_loaded('gd') ) return ''; $mem = (int)ini_get('memory_limit'); if ( $mem > 0 && $mem < 128 ) ini_set('memory_limit','128M'); $W = self::W; $H = self::H; $photoH = (int)($H * 0.62); $bandH = $H - $photoH; $cat = $article['category'] ?? 'Societe'; // Normaliser les clés avec accents $cat_map = [ 'Politique'=>'Politique','Economie'=>'Economie','Societe'=>'Societe', 'Diaspora'=>'Diaspora','Region'=>'Region','Culture'=>'Culture','Sport'=>'Sport', 'Économie'=>'Economie','Société'=>'Societe','Région'=>'Region', ]; $cat_key = isset($cat_map[$cat]) ? $cat_map[$cat] : 'Societe'; $rgb = self::CAT_COLORS[$cat_key] ?? self::CAT_COLORS['Societe']; // 1. Photo (source -> page OG/image -> génération IA OpenAI en fallback) $visual_source = $this->resolve_photo_source($photo_url, $article); $band_rgb = $this->resolve_banner_rgb($visual_source, $rgb); $img = imagecreatetruecolor($W, $H); $band = imagecolorallocate($img, $band_rgb[0], $band_rgb[1], $band_rgb[2]); $wht = imagecolorallocate($img, 255, 255, 255); $blk = imagecolorallocate($img, 17, 17, 17); imagefilledrectangle($img, 0, 0, $W, $H, $band); $this->draw_photo($img, $visual_source, $W, $photoH); imagefilledrectangle($img, 0, $photoH, $W, $H, $band); // 2. Logo AI cercle parfait $this->draw_logo_ai($img, $blk, $W); // 3. Calculer position logo bannière AVANT le titre // Réserver plus d'espace vertical pour éviter toute superposition // avec les titres longs ou les logos de bannière plus hauts que prévu. $logo_w = (int)($W * 0.64); $logo_h = max(72, (int)($bandH * 0.20)); $logo_x = (int)(($W - $logo_w) / 2); $logo_gap = max(18, (int)($bandH * 0.06)); $logo_y = $H - $logo_h - (int)($bandH * 0.07); // 4. Titre avec taille automatique, en gardant une vraie zone tampon // au-dessus du logo du bas. $this->draw_title($img, $article['title_fr'] ?? '', $wht, $photoH, $bandH, $W, $logo_y, $logo_gap, $logo_h); // 5. Logo bannière ancré en bas $this->draw_logo_banner($img, $W, $logo_w, $logo_h, $logo_x, $logo_y); $file = 'visual-' . time() . '-' . wp_rand(1000,9999) . '.jpg'; $path = $this->upload_dir . $file; imagejpeg($img, $path, 92); imagedestroy($img); return $path; } private function get_logo_url( int $id ): string { if ( ! $id ) return ''; return wp_get_attachment_url($id) ?: ''; } private function resolve_photo_source( string $photo_url, array $article ): string { if ( ! empty($photo_url) && $this->looks_like_remote_image($photo_url) ) return $photo_url; $source_url = (string)($article['source_url'] ?? ''); if ( $source_url !== '' ) { $found = $this->extract_image_from_article_url($source_url); if ( ! empty($found) ) return $found; } if ( $this->provider === 'openai' && ! empty($this->api_key) ) { $generated = $this->generate_ai_photo((string)($article['title_fr'] ?? ''), (string)($article['category'] ?? '')); if ( ! empty($generated) ) return $generated; } return ''; } private function extract_image_from_article_url( string $url ): string { $response = wp_remote_get($url, [ 'timeout' => 8, 'sslverify' => false, 'user-agent' => 'ArmenieInfo/4.0', ]); if ( is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200 ) return ''; $html = wp_remote_retrieve_body($response); if ( empty($html) ) return ''; if ( strlen($html) > 250000 ) $html = substr($html, 0, 250000); $base = $this->base_from_url($url); $candidates = []; $patterns = [ '/]+property=["\']og:image(?::url)?["\'][^>]+content=["\']([^"\']+)["\']/i', '/]+name=["\']twitter:image(?::src)?["\'][^>]+content=["\']([^"\']+)["\']/i', '/]+property=["\']og:image:secure_url["\'][^>]+content=["\']([^"\']+)["\']/i', '/]+rel=["\']image_src["\'][^>]+href=["\']([^"\']+)["\']/i', '/]+(?:data-lazy-src|data-original|data-src|data-image|src)=["\']([^"\']+)["\'][^>]*>/i', ]; foreach ( $patterns as $pattern ) { if ( preg_match_all($pattern, $html, $matches) ) { foreach ( $matches[1] as $raw ) { $candidate = $this->abs_url(trim(html_entity_decode($raw, ENT_QUOTES, 'UTF-8')), $base); if ( $this->looks_like_remote_image($candidate) ) $candidates[] = $candidate; } } } if ( preg_match_all('/(?:\"image\"|\"thumbnailUrl\"|\"contentUrl\")\s*:\s*\"(https?:\\/\\/[^"\\]+)\"/i', $html, $matches) ) { foreach ( $matches[1] as $raw ) { $candidate = stripslashes($raw); if ( $this->looks_like_remote_image($candidate) ) $candidates[] = $candidate; } } if ( preg_match_all('/srcset=["\']([^"\']+)["\']/i', $html, $srcsets) ) { foreach ( $srcsets[1] as $srcset ) { $parts = preg_split('/\s*,\s*/', $srcset); foreach ( $parts as $part ) { $candidate = trim((string)preg_replace('/\s+\d+[wx]$/i', '', $part)); $candidate = $this->abs_url($candidate, $base); if ( $this->looks_like_remote_image($candidate) ) $candidates[] = $candidate; } } } $civic_match = $this->extract_civic_image_for_article($html, $url, $base); if ( ! empty($civic_match) ) array_unshift($candidates, $civic_match); return $this->select_best_image_candidate($candidates, $base); } private function extract_civic_image_for_article( string $html, string $article_url, string $base = '' ): string { $host = strtolower((string)parse_url($base ?: $article_url, PHP_URL_HOST)); if ( ! str_contains($host, 'civic.am') ) return ''; if ( ! preg_match('#/(?:news|politics|society|economy|culture|international|article)/(\d+)#i', $article_url, $id_match) ) return ''; $article_id = $id_match[1]; $quoted_id = preg_quote($article_id, '#'); $candidates = []; $patterns = [ '#]+href=["\"][^"\"]*/' . $quoted_id . '(?:/[^"\"]*)?["\"][^>]*>.*?]+(?:data-lazy-src|data-original|data-src|data-image|src)=["\"]([^"\"]+)["\"][^>]*>#is', '#]+(?:data-lazy-src|data-original|data-src|data-image|src)=["\"]([^"\"]+)["\"][^>]*>.*?]+href=["\"][^"\"]*/' . $quoted_id . '(?:/[^"\"]*)?["\"][^>]*>#is', '#https?://(?:www\.)?civic\.am/thumbs/108x108/[^"\'\s>]+#i', '#https?://(?:www\.)?civic\.am/thumbs/696x444/[^"\'\s>]+#i', ]; foreach ( $patterns as $idx => $pattern ) { if ( ! preg_match_all($pattern, $html, $matches, PREG_SET_ORDER) ) continue; foreach ( $matches as $match ) { $raw = $idx < 2 ? ($match[1] ?? '') : ($match[0] ?? ''); $candidate = $this->abs_url(trim(html_entity_decode((string)$raw, ENT_QUOTES, 'UTF-8')), $base ?: $article_url); if ( ! $this->is_usable_photo_url($candidate) && ! $this->looks_like_remote_image($candidate) ) continue; $candidates[] = $candidate; } } if ( preg_match_all('#]+href=["\"][^"\"]*/' . $quoted_id . '(?:/[^"\"]*)?["\"][^>]*>(.*?)#is', $html, $blocks) ) { foreach ( $blocks[1] as $block ) { if ( preg_match_all('#(?:data-lazy-src|data-original|data-src|data-image|src)=["\"]([^"\"]+)["\"]#i', $block, $img_matches) ) { foreach ( $img_matches[1] as $raw ) { $candidate = $this->abs_url(trim(html_entity_decode((string)$raw, ENT_QUOTES, 'UTF-8')), $base ?: $article_url); if ( ! $this->is_usable_photo_url($candidate) && ! $this->looks_like_remote_image($candidate) ) continue; $candidates[] = $candidate; } } } } $candidates = array_values(array_unique(array_filter($candidates))); if ( empty($candidates) ) return ''; $best = ''; $best_score = -999999; foreach ( $candidates as $candidate ) { $score = $this->score_image_candidate($candidate, $base ?: $article_url); if ( str_contains(strtolower($candidate), '/thumbs/108x108/') ) $score += 15000; if ( preg_match('#/' . $quoted_id . '(?:[^0-9]|$)#', $html) ) $score += 1000; if ( $score > $best_score ) { $best_score = $score; $best = $candidate; } } if ( $best === '' ) return ''; if ( preg_match('#/thumbs/108x108/#i', $best) ) { return preg_replace('#/thumbs/108x108/#i', '/thumbs/696x444/', $best, 1) ?: $best; } return $best; } private function select_best_image_candidate( array $candidates, string $base = '' ): string { $candidates = array_values(array_unique(array_filter($candidates))); if ( empty($candidates) ) return ''; $best = ''; $best_score = -999999; foreach ( $candidates as $candidate ) { $score = $this->score_image_candidate($candidate, $base); if ( $score > $best_score ) { $best_score = $score; $best = $candidate; } } return $best; } private function score_image_candidate( string $url, string $base = '' ): int { $score = 0; $u = strtolower($url); $host = strtolower((string)parse_url($base, PHP_URL_HOST)); if ( preg_match('#/(\d{2,4})x(\d{2,4})/#i', $u, $m) ) { $w = (int)$m[1]; $h = (int)$m[2]; $area = $w * $h; $score += (int)min(120000, $area / 20); if ( $w >= 600 || $h >= 400 ) $score += 5000; if ( $w <= 180 && $h <= 180 ) $score -= 25000; } if ( preg_match('#[?&](?:w|width)=(\d{2,4})#i', $u, $mw) ) $score += min(12000, (int)$mw[1] * 8); if ( preg_match('#[?&](?:h|height)=(\d{2,4})#i', $u, $mh) ) $score += min(8000, (int)$mh[1] * 5); foreach ( ['og-image', 'opengraph', 'featured', 'feature', 'hero', 'cover', 'full', 'large', 'original', 'main', 'content', 'post', 'article'] as $good ) { if ( str_contains($u, $good) ) $score += 2500; } foreach ( ['thumb', 'thumbnail', 'small', 'tiny', 'avatar', 'profile', 'icon', 'logo', 'placeholder', 'lazy', 'default'] as $bad ) { if ( str_contains($u, $bad) ) $score -= 3500; } if ( str_contains($u, '.webp') ) $score += 300; if ( str_contains($u, '.jpg') || str_contains($u, '.jpeg') ) $score += 200; $score += min(2000, strlen($u)); if ( $host !== '' && str_contains($u, $host) ) $score += 800; if ( str_contains($host, 'civic.am') ) { if ( preg_match('#/thumbs/696x444/#i', $u) ) $score += 50000; if ( preg_match('#/thumbs/108x108/#i', $u) ) $score -= 50000; } return $score; } private function resolve_banner_rgb( string $visual_source, array $fallback_rgb ): array { $rgb = $this->extract_banner_rgb_from_source($visual_source); if ( empty($rgb) ) return $fallback_rgb; return $rgb; } private function extract_banner_rgb_from_source( string $visual_source ): array { if ( empty($visual_source) ) return []; if ( file_exists($visual_source) ) { $data = @file_get_contents($visual_source); } else { $ctx = stream_context_create([ 'http' => ['timeout' => 8, 'user_agent' => 'ArmenieInfo/4.0'], 'ssl' => ['verify_peer' => false], ]); $data = @file_get_contents($visual_source, false, $ctx); } if ( ! $data ) return []; $src = @imagecreatefromstring($data); unset($data); if ( ! $src ) return []; $src_w = imagesx($src); $src_h = imagesy($src); if ( $src_w <= 0 || $src_h <= 0 ) { imagedestroy($src); return []; } $sample = 32; $thumb = imagecreatetruecolor($sample, $sample); imagecopyresampled($thumb, $src, 0, 0, 0, 0, $sample, $sample, $src_w, $src_h); imagedestroy($src); $sum_r = 0; $sum_g = 0; $sum_b = 0; $count = 0; for ( $x = 0; $x < $sample; $x++ ) { for ( $y = (int)($sample * 0.55); $y < $sample; $y++ ) { $px = imagecolorat($thumb, $x, $y); $r = ($px >> 16) & 0xFF; $g = ($px >> 8) & 0xFF; $b = $px & 0xFF; $sum_r += $r; $sum_g += $g; $sum_b += $b; $count++; } } imagedestroy($thumb); if ( $count === 0 ) return []; $r = (int)round($sum_r / $count); $g = (int)round($sum_g / $count); $b = (int)round($sum_b / $count); $r = max(12, (int)round($r * 0.52)); $g = max(12, (int)round($g * 0.52)); $b = max(12, (int)round($b * 0.52)); return [ $r, $g, $b ]; } private function generate_ai_photo( string $title, string $category = '' ): string { $title = trim($title); if ( $title === '' ) return ''; $prompt = 'Illustration photojournalistique réaliste, sans texte, sans logo, sans watermark. Sujet: ' . $title . '.'; if ( $category !== '' ) { $prompt .= ' Contexte éditorial: ' . $category . '.'; } $prompt .= ' Format vertical 4:5, composition propre, style photo de presse moderne.'; $response = wp_remote_post('https://api.openai.com/v1/images/generations', [ 'timeout' => 120, 'headers' => [ 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->api_key, ], 'body' => wp_json_encode([ 'model' => 'gpt-image-1', 'prompt' => $prompt, 'size' => '1024x1536', 'response_format' => 'b64_json', ]), ]); if ( is_wp_error($response) || wp_remote_retrieve_response_code($response) >= 300 ) return ''; $data = json_decode(wp_remote_retrieve_body($response), true); $b64 = $data['data'][0]['b64_json'] ?? ''; if ( empty($b64) ) return ''; $bin = base64_decode($b64); if ( ! $bin ) return ''; $file = $this->upload_dir . 'generated-' . time() . '-' . wp_rand(1000, 9999) . '.png'; if ( file_put_contents($file, $bin) === false ) return ''; return $file; } private function base_from_url( string $url ): string { $scheme = parse_url($url, PHP_URL_SCHEME) ?: 'https'; $host = parse_url($url, PHP_URL_HOST) ?: ''; return $host ? $scheme . '://' . $host : $url; } private function abs_url( string $url, string $base ): string { if ( $url === '' ) return ''; if ( strpos($url, 'http') === 0 ) return $url; if ( strpos($url, '//') === 0 ) return 'https:' . $url; if ( strpos($url, '/') === 0 ) return rtrim($base, '/') . $url; return rtrim($base, '/') . '/' . ltrim($url, '/'); } private function looks_like_remote_image( string $url ): bool { if ( $url === '' || str_contains($url, 'data:image') ) return false; if ( ! preg_match('#^https?://#i', $url) ) return false; $bad = ['/logo.', '/logo_', '/icon.', '/icon_', 'favicon', 'sprite', 'placeholder', 'default-image', '/blank.']; $u = strtolower($url); foreach ( $bad as $needle ) { if ( str_contains($u, $needle) ) return false; } return true; } // ── Photo ────────────────────────────────────────────────────────────── private function draw_photo( $img, string $url, int $W, int $pH ): void { if ( empty($url) ) return; if ( file_exists($url) ) { $data = @file_get_contents($url); } else { $ctx = stream_context_create([ 'http' => ['timeout' => 8, 'user_agent' => 'ArmenieInfo/4.0'], 'ssl' => ['verify_peer' => false], ]); $data = @file_get_contents($url, false, $ctx); } if ( ! $data ) return; $src = @imagecreatefromstring($data); unset($data); if ( ! $src ) return; $src_w = imagesx($src); $src_h = imagesy($src); if ( $src_w <= 0 || $src_h <= 0 ) { imagedestroy($src); return; } $dst_ratio = $W / $pH; $src_ratio = $src_w / $src_h; if ( $src_ratio > $dst_ratio ) { // Image plus large que la zone : on coupe à gauche/droite, centré. $crop_h = $src_h; $crop_w = (int) round($src_h * $dst_ratio); $src_x = (int) max(0, round(($src_w - $crop_w) / 2)); $src_y = 0; } else { // Image plus haute que la zone : on coupe en haut/bas, centré. $crop_w = $src_w; $crop_h = (int) round($src_w / $dst_ratio); $src_x = 0; $src_y = (int) max(0, round(($src_h - $crop_h) / 2)); } imagecopyresampled( $img, $src, 0, 0, $src_x, $src_y, $W, $pH, max(1, $crop_w), max(1, $crop_h) ); imagedestroy($src); } // ── Logo AI : VRAI cercle parfait antialiasing 4x ────────────────────── private function draw_logo_ai( $img, $blk, int $W ): void { $sz = (int)($W * 0.155); $mg = (int)($W * 0.037); // Disque blanc haute résolution 4x → antialiasing parfait $hr = $sz * 4; $disc_hr = imagecreatetruecolor($hr, $hr); imagealphablending($disc_hr, false); imagesavealpha($disc_hr, true); $transp = imagecolorallocatealpha($disc_hr, 0, 0, 0, 127); imagefill($disc_hr, 0, 0, $transp); $white_hr = imagecolorallocate($disc_hr, 255, 255, 255); imagefilledellipse($disc_hr, $hr/2, $hr/2, $hr, $hr, $white_hr); // Redimensionner à taille finale (antialiasing) $disc = imagecreatetruecolor($sz, $sz); imagealphablending($disc, false); imagesavealpha($disc, true); imagecopyresampled($disc, $disc_hr, 0, 0, 0, 0, $sz, $sz, $hr, $hr); imagedestroy($disc_hr); imagecopy($img, $disc, $mg, $mg, 0, 0, $sz, $sz); imagedestroy($disc); // Logo rogné en cercle $url = $this->get_logo_url($this->logo_ai_id); if ( $url ) { $ctx = stream_context_create(['http'=>['timeout'=>5],'ssl'=>['verify_peer'=>false]]); $data = @file_get_contents($url, false, $ctx); if ( $data ) { $logo = @imagecreatefromstring($data); if ( $logo ) { $lw = imagesx($logo); $lh = imagesy($logo); $inner = (int)($sz * 0.82); $offset = (int)(($sz - $inner) / 2); // Redimensionner le logo $logo_r = imagecreatetruecolor($inner, $inner); imagealphablending($logo_r, true); $white = imagecolorallocate($logo_r, 255, 255, 255); imagefill($logo_r, 0, 0, $white); imagecopyresampled($logo_r, $logo, 0, 0, 0, 0, $inner, $inner, $lw, $lh); imagedestroy($logo); // Masque circulaire HR $mhr = $inner * 4; $mask_hr = imagecreatetruecolor($mhr, $mhr); imagealphablending($mask_hr, false); imagesavealpha($mask_hr, true); $t = imagecolorallocatealpha($mask_hr, 0, 0, 0, 127); imagefill($mask_hr, 0, 0, $t); imagefilledellipse($mask_hr, $mhr/2, $mhr/2, $mhr, $mhr, imagecolorallocate($mask_hr, 255, 255, 255)); // Appliquer le masque pixel par pixel for ( $x = 0; $x < $inner; $x++ ) { for ( $y = 0; $y < $inner; $y++ ) { $mp = imagecolorat($mask_hr, $x*4, $y*4); $a = ($mp >> 24) & 0x7F; if ( $a < 80 ) { $sp = imagecolorat($logo_r, $x, $y); $r = ($sp >> 16) & 0xFF; $g = ($sp >> 8) & 0xFF; $b = $sp & 0xFF; imagesetpixel($img, $mg + $offset + $x, $mg + $offset + $y, imagecolorallocate($img, $r, $g, $b)); } } } imagedestroy($logo_r); imagedestroy($mask_hr); unset($data); return; } } } // Fallback texte "AI" $font = $this->find_font(); $fs = (int)($sz * 0.38); $cx = $mg + (int)($sz / 2); $cy = $mg + (int)($sz / 2); if ( $font && function_exists('imagettftext') ) { $bb = imagettfbbox($fs, 0, $font, 'AI'); imagettftext($img, $fs, 0, $cx - (int)(abs($bb[2]-$bb[0])/2), $cy + (int)(abs($bb[7]-$bb[1])/2), $blk, $font, 'AI'); } else { imagestring($img, 5, $cx-20, $cy-12, 'AI', $blk); } } // ── Titre : taille automatique, centré, jamais sur logo bannière ──────── private function draw_title( $img, string $title, $wht, int $photoH, int $bandH, int $W, int $logo_y, int $logo_gap = 24, int $logo_h = 0 ): void { $padX = (int)($W * 0.05); $maxW = $W - ($padX * 2); $font = $this->find_font(); if ( ! $font || ! function_exists('imagettftext') ) return; // Trouver la taille de police qui rentre dans la zone disponible $zone_top = $photoH + (int)($bandH * 0.07); $zone_bottom = $logo_y - $logo_gap; $zone_h = max(80, $zone_bottom - $zone_top); $best_fs = 32; $best_lines = [ $title ]; for ( $fs = 80; $fs >= 28; $fs -= 2 ) { $line_h = (int)($fs * 1.24); // Word wrap $lines = []; $words = preg_split('/\s+/', trim($title)) ?: []; $cur = ''; foreach ( $words as $word ) { $test = $cur !== '' ? "$cur $word" : $word; $bb = imagettfbbox($fs, 0, $font, $test); $test_w = abs($bb[2] - $bb[0]); if ( $test_w > $maxW && $cur !== '' ) { $lines[] = $cur; $cur = $word; } else { $cur = $test; } } if ( $cur !== '' ) $lines[] = $cur; if ( empty($lines) ) $lines = [ $title ]; $total_h = count($lines) * $line_h; $fits_height = $total_h <= (int)($zone_h * 0.88); $fits_width = true; foreach ( $lines as $line ) { $bb = imagettfbbox($fs, 0, $font, $line); if ( abs($bb[2] - $bb[0]) > $maxW ) { $fits_width = false; break; } } if ( $fits_height && $fits_width ) { $best_fs = $fs; $best_lines = $lines; break; } } $line_h = (int)($best_fs * 1.24); $text_h = count($best_lines) * $line_h; $y = $zone_top + (int)(($zone_h - $text_h) / 2) + $line_h; $vivid = $this->extract_vivid_color($img, $W, $photoH); $accents = [ imagecolorallocate($img, $vivid[0], $vivid[1], $vivid[2]), imagecolorallocate($img, $vivid[2], $vivid[0], $vivid[1]), imagecolorallocate($img, min(255,$vivid[1]+100), min(255,$vivid[0]+60), max(0,$vivid[2]-80)), ]; $proper_noun_idx = 0; $title_word_pos = 0; foreach ( $best_lines as $line ) { $bb = imagettfbbox($best_fs, 0, $font, $line); $line_w = abs($bb[2] - $bb[0]); $x = max($padX, (int)(($W - $line_w) / 2)); $blk = imagecolorallocate($img, 0, 0, 0); imagettftext($img, $best_fs, 0, $x-2, $y-2, $blk, $font, $line); imagettftext($img, $best_fs, 0, $x+2, $y-2, $blk, $font, $line); imagettftext($img, $best_fs, 0, $x-2, $y+2, $blk, $font, $line); imagettftext($img, $best_fs, 0, $x+2, $y+2, $blk, $font, $line); imagettftext($img, $best_fs, 0, $x, $y-2, $blk, $font, $line); imagettftext($img, $best_fs, 0, $x, $y+2, $blk, $font, $line); imagettftext($img, $best_fs, 0, $x-2, $y, $blk, $font, $line); imagettftext($img, $best_fs, 0, $x+2, $y, $blk, $font, $line); $words_in_line = preg_split('/\s+/', trim($line)) ?: [$line]; $wx = $x; foreach ($words_in_line as $w) { if ($w === '') continue; $is_proper = ($title_word_pos > 0 && mb_strlen($w) > 2 && ctype_upper(mb_substr($w, 0, 1))); imagettftext($img, $best_fs, 0, $wx, $y, $is_proper ? $accents[$proper_noun_idx % 3] : $wht, $font, $w); if ($is_proper) $proper_noun_idx++; $wb = imagettfbbox($best_fs, 0, $font, $w . ' '); $wx += abs($wb[2] - $wb[0]); $title_word_pos++; } $y += $line_h; } } // ── Logo bannière : ancré en bas, jamais sur le texte ─────────────────── private function draw_logo_banner( $img, int $W, int $lw, int $lh, int $lx, int $ly ): void { $wht = imagecolorallocate($img, 255, 255, 255); imagefilledrectangle($img, $lx, $ly, $lx + $lw, $ly + $lh, $wht); $url = $this->get_logo_url($this->logo_banner_id); if ( ! $url ) return; $ctx = stream_context_create(['http'=>['timeout'=>5],'ssl'=>['verify_peer'=>false]]); $data = @file_get_contents($url, false, $ctx); if ( ! $data ) return; $logo = @imagecreatefromstring($data); if ( ! $logo ) return; $bw = imagesx($logo); $bh = imagesy($logo); $s = min(($lw * 0.88) / $bw, ($lh * 0.80) / $bh); $nw = (int)($bw * $s); $nh = (int)($bh * $s); $dx = $lx + (int)(($lw - $nw) / 2); $dy = $ly + (int)(($lh - $nh) / 2); imagecopyresampled($img, $logo, $dx, $dy, 0, 0, $nw, $nh, $bw, $bh); imagedestroy($logo); unset($data); } private function find_font(): string { foreach ([ AIAG_DIR . 'assets/font.ttf', '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', '/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf', '/usr/share/fonts/dejavu/DejaVuSans-Bold.ttf', 'C:\\Windows\\Fonts\\arialbd.ttf', ] as $f) { if ( file_exists($f) ) return $f; } $dest = AIAG_DIR . 'assets/font.ttf'; $ctx = stream_context_create(['http'=>['timeout'=>15],'ssl'=>['verify_peer'=>false]]); $data = @file_get_contents('https://github.com/dejavu-fonts/dejavu-fonts/raw/master/ttf/DejaVuSans-Bold.ttf', false, $ctx); if ($data && strlen($data) > 10000) { @file_put_contents($dest, $data); return $dest; } return ''; } private function extract_vivid_color( $img, int $w, int $max_y ): array { $best_sat = 0; $best = [255, 200, 50]; $step = 40; for ( $x = $step; $x < $w - $step; $x += $step ) { for ( $y = $step; $y < $max_y - $step; $y += $step ) { $px = imagecolorat( $img, $x, $y ); $r = ( $px >> 16 ) & 0xFF; $g = ( $px >> 8 ) & 0xFF; $b = $px & 0xFF; $mx = max( $r, $g, $b ); $mn = min( $r, $g, $b ); $sat = $mx - $mn; if ( $sat > $best_sat && $mx > 60 ) { $best_sat = $sat; $best = [ $r, $g, $b ]; } } } $mx = max( $best ); if ( $mx > 0 && $mx < 160 ) { $f = 220 / $mx; $best = [ min(255,(int)($best[0]*$f)), min(255,(int)($best[1]*$f)), min(255,(int)($best[2]*$f)) ]; } return $best; } }Archives des Djermouk - Arménie Info

Étiquette : Djermouk