og:image

Générer une image de partage avec GD

Ça ne marche pas

Aujourd’hui j’ai décidé de m’attaquer à mon propre site web. Lorsque je partage un article sur LinkedIn, à chaque fois, je construis une image en Photoshop que j’associe au post. C’est vraiment moche je sais… En plus, de cette manière, lorsque l’image est cliquée, elle ne renvoie même pas sur l’article mais s’affiche bêtement dans LinkedIn.

L’objectif va être de créer un système qui récupère différentes données et qui génère une image automatiquement.

GD

J’avais récemment travaillé sur du « Pixel art » et bien je vais reprendre la même technologie pour construire l’image.

Données Wordpress

Mon site tourne dans un WordPress (oui je sais il est très rapide), donc j’ai une structure bien particulière. Je vais créer un fichier PHP dans le dossier libs de mon thème qui me servira uniquement à construire l’image, rien d’autre ! Pour cela je vais déjà placer une entête particulière pour sortir une image (ligne 1 dans le code).

Pour récupérer les données de mon article, comme je suis dans un WordPress, je vais devoir charger le noyau et balancer deux ou trois fonctions pour récupérer le titre, le sous-titre, la catégorie de l’article et l’image associée s’il y en a une.

header('Content-Type: image/jpg');

if( isset($_GET["id"]) && $_GET["id"] > 0 ){
	
	include "../../../../wp-load.php";
	
	$id 		= $_GET["id"];
	$post 		= get_post($id);
	$acf	 	= get_fields($id);
	$category 	= current(get_the_category($id));
	
}

Largeur et hauteur

Juste en dessous, je crée deux variables qui vont me permettre de définir la largeur et la hauteur de mon image.
Etant donné que j’ai des vignettes carrées sur mon site et que LinkedIn accepte un format de 1:1.90, je vais être obligé de cropper l’image.

J’ai utilisé une troisième variable qui va simplement correspondre à la largeur, même si le nom fait référence à de la hauteur. C’est juste pour faire la transition entre mes vignettes carrées et le format 1:1.90. Je procède de cette façon pour avoir un nom différent et ne pas me mélanger les pinceaux dans les fonctions de GD.

$linkedInWidth = 600;
$linkedInHeight = 315;
$virtualHeightForImage = $linkedInWidth;

Un peu de couleur

Je vais préalablement créer un tableau de couleurs, ça me permettra d’utiliser telle ou telle couleur en fonction de la catégorie de l’article.

$colors = array(
	"css" 			=> array("#6fce4a", "#ffffff"),
	"php" 			=> array("#195d82", "#ffffff"),
	"media" 		=> array("#ff5a00", "#ffffff"),
	"wordpress" 		=> array("#ffff33", "#000000"),
	"autre" 		=> array("#b6f4ff", "#303030"),
	"parcours" 		=> array("#ffffff", "#303030"),
	"comment" 		=> array("#ed145b", "#ffffff"),
	"cli" 			=> array("#f2e5d5", "#000000"),
	"javascript" 		=> array("#d92b4b", "#ffffff"),
	"serveur" 		=> array("#EA6783", "#000000"),
	"sgbd" 			=> array("#ececec", "#000000"),
	"jeu" 			=> array("#ff5a00", "#ffffff"),
);

Le masque de couleur

La première étape consiste à faire une image de couleur correspondante à la catégorie de l’article. Je convertis la couleur qui est un hexadécimal via un sscanf pour obtenir des valeurs rgb. Je crée une image que je stocke dans la variable image_color ainsi que la couleur via la fonction imagecolorallocate et je l’applique sur l’image grâce à la fonction imagefill. Enfin, j’affiche le résultat qui sera une image.

/*****************************************************
*
*	COULEUR
*
*****************************************************/
list($r, $g, $b) = sscanf($colors[$category->slug][0], "#%02x%02x%02x");
$image_color = imagecreatetruecolor($linkedInWidth, $linkedInHeight);
$color = imagecolorallocate($image_color, $r, $g, $b);
imagefill($image_color, 0, 0, $color);

imagejpeg($image_color);

L'image vignette

Maintenant je vais m’occuper de récupérer la vignette carrée de mon article et l’injecter dans l’image que je viens de construire. Avant toute chose, je vérifie s’il y a une image associée à mon article, sinon je peux passer cette étape.

En fonction du type d’image de la vignette (jpg/png ou gif), j’execute la fonction correspondante pour me permettre de charger l’image dans une ressource.

/*****************************************************
*
*	PHOTO
*
*****************************************************/
if( isset($acf["image_pour_la_box"]["url"]) && $acf["image_pour_la_box"]["url"] != ""){
	
	if( $acf["image_pour_la_box"]["subtype"] == "jpeg" ){
		$image = imagecreatefromjpeg($acf["image_pour_la_box"]["url"]);	
	}elseif( $acf["image_pour_la_box"]["subtype"] == "png" ){
		$image = imagecreatefrompng($acf["image_pour_la_box"]["url"]);	
	}elseif( $acf["image_pour_la_box"]["subtype"] == "gif" ){
		$image = imagecreatefromgif($acf["image_pour_la_box"]["url"]);	
	}

}

Je récupère la largeur et la hauteur de la vignette pour avoir les informations nécessaires pour effectuer un redimensionnement. Je crée une nouvelle ressource image qui aura les dimensions attendues de l’image de partage.

Via la fonction imagecopyresized, je vais copier l’image vignette et la redimensionner dans la nouvelle ressource image. En fait, ce qui va se passer avec ces paramètres, c’est que l’image carrée va être de la même largeur que la nouvelle image. Vu que celle-ci a un format rectangulaire (hauteur plus petite que la largeur), l’image vignette va donc déborder (voir ci-dessous), du coup je la remonte de la moitié de son dépassement pour la centrer correctement. C’est typiquement un cover.

/*****************************************************
*
*	PHOTO
*
*****************************************************/
if( isset($acf["image_pour_la_box"]["url"]) && $acf["image_pour_la_box"]["url"] != ""){
	
	//...

	list($width, $height) = getimagesize($acf["image_pour_la_box"]["url"]);
	$new_image = imagecreatetruecolor($linkedInWidth, $linkedInHeight);
	
	imagecopyresized($new_image, $image, 0, intval(($linkedInHeight - $linkedInWidth) / 2), 0, 0, $linkedInWidth, $virtualHeightForImage, $width, $height);
}

Ci-dessous une vue d’artiste du positionnement de l’image de la vignette (carrée) par rapport à l’image en couleur (rectangulaire).

Si j’affiche $new_image juste après la fonction imagecopyresized voici le résultat que j’obtiens.

Pour obtenir le même effet que sur mon site, je passe l’image en niveau de gris via l’application d’un filtre. Ensuite je fusionne l’image en couleur avec la photo et je choisis 12.7 qui correspond à une opacité de 10%.

/*****************************************************
*
*	PHOTO
*
*****************************************************/
if( isset($acf["image_pour_la_box"]["url"]) && $acf["image_pour_la_box"]["url"] != ""){
	
	//...

	imagefilter($new_image, IMG_FILTER_GRAYSCALE);
	imagecopymerge($image_color, $new_image, 0, 0, 0, 0, $linkedInWidth, $linkedInHeight, 12.7);
}

Le texte

Ecrire du texte en GD c’est simple, mais écrire du texte centré c’est une autre paire de manches. Sur mon site, le titre et le sous-titre diffèrent entre chaque page, il va donc falloir calculer pour chaque article la position exacte du texte.

Je vais dans un premier temps récupérer mon texte et le splitter dans un tableau. Le délimiteur sera un retour à la ligne. En gros, chaque élément du tableau va correspondre à un nouvelle ligne.

Et j’execute une fonction qui va me permettre d’écrire ces différentes lignes.

if( isset($acf["titre_du_partage"]) && $acf["titre_du_partage"] != "" ){
	$title = explode("\n", $acf["titre_du_partage"]);
}else{
	$title = array($post->post_title);
}
if( isset($acf["texte_du_partage"]) && $acf["texte_du_partage"] != "" ){
	$content = explode("\n", $acf["texte_du_partage"]);
}else{
	$content = array($post->post_content);
}

writeTitleAndContent($image_color, $title, $content, sscanf($colors[$category->slug][1], "#%02x%02x%02x"));
imagejpeg($image_color);

La fonction writeTitleAndContent prendra en paramètre l’image générée précédemment (passage par référence pour éviter de faire des return) le titre, le contenu et la couleur du texte.

Je définis quelques variables dans la fonction comme l’URL de la font à utiliser, la taille de la typo, l’angle, un offset dans l’axe Y qui m’évitera d’écrire contre le bord de l’image ainsi qu’un offset entre les lignes.

Ensuite, je claque une boucle qui va passer sur chaque élément du titre et qui va executer une nouvelle fonction pour obtenir les données de placement. J’écris chaque ligne de texte sur l’image via la fonction imagettftext, j’ajuste la position Y avec l’offset et je calcule $currentY qui est la position Y du prochain élément.

function writeTitleAndContent(&$image_color, $title, $content, $textColor){
	$font = "../stylesheets/fonts/pacifico-webfont.ttf";
	list($r, $g, $b) = $textColor;
	$textColor = imagecolorallocate($image_color, $r, $g, $b);
	$fontSize = 30;
	$angle = 0;
	$yOffset = 30;
	$lineOffset = 10;
	
	$currentY = $yOffset;
	
	//FOR TITLE
	foreach($title as $key => $string){
		$data = calculateXforCenterText($fontSize, $angle, $font, $string);
		imagettftext($image_color, $data["fontSize"], $angle, $data["x"], $data["y"] + $currentY, $textColor, $font, $string);
		$currentY += $data["y"] + $lineOffset;
	}
}

La fonction calculateXforCenterText est assez simple en soit, elle va placer une boîte autour du texte en fonction de la typo et de la taille de la font, ce qui va me permettre d’obtenir sa largeur.

Une fois que j’ai obtenu la largeur, je soustrais la largeur totale possible avec la largeur du texte et je la divise par deux, ce qui permet d’obtenir la position X du texte pour le centrer.

Avant de retourner le résultat, je vérifie si le texte n’est pas trop grand grâce à une variable $toleranceBorder et je m’assure que le texte n’est jamais à moins de 25px d’écart d’une bordure. Si c’est le cas, alors je fais un peu de récursivité et je recalcule une taille de typo adaptée. En fait, je réduis la typo de 1px et je répète systématiquement cette action tant que le texte ne passe pas.

Dès que c’est bon, je retourne les données, c’est-à-dire la position X et Y du texte (Y correspond à la hauteur de la boîte englobant le texte) et bien sûr la nouvelle taille de font, si jamais je l’avais ajusté.

function calculateXforCenterText($fontSize, $angle, $font, $text, $toleranceBorder = 25){
	global $linkedInWidth;
	$bbox = imagettfbbox($fontSize, $angle, $font, $text);
	$textWidth = $bbox[2] - $bbox[0];
	$newX = intval( ($linkedInWidth - $textWidth) / 2);
	if( $newX < $toleranceBorder ){
		return calculateXforCenterText($fontSize-1, $angle, $font, $text);
	}else{
		return array("x" => $newX, "y" => abs($bbox[5]) + abs($bbox[3]), "fontSize" => $fontSize);
	}
}

Au niveau du titre c’est parfait ! Dans la fonction writeTitleAndContent juste en dessous du premier foreach, je modifie quelques variables comme la font et la taille de typo pour pouvoir écrire le contenu, j’ajoute la valeur de l’offset à la position Y histoire de mettre une marge entre le titre et le contenu. Ensuite, je rebalance exactement la même boucle mais sur le contenu.

function writeTitleAndContent(&$image_color, $title, $content, $textColor){
	
	//FOR TITLE
	//...
	
	//FOR CONTENT
	$currentY += $yOffset;
	$font = "../stylesheets/fonts/sourcesanspro-regular-webfont.ttf";
	$fontSize = 15;

	foreach($content as $key => $string){
		$data = calculateXforCenterText($fontSize, $angle, $font, $string);
		imagettftext($image_color, $data["fontSize"], $angle, $data["x"], $data["y"] + $currentY, $textColor, $font, $string);
		$currentY += $data["y"] + $lineOffset;
	}
}

META

Et bien sûr, à l’intérieur du head de mes pages d’articles, j’ajoute une meta og:image qui va faire référence à mon script PHP en n’oubliant pas le paramètre ID qui correspond à l’identifiant de mon post, et le tour est joué !

if( is_single() ){
	?><meta property="og:image" content="/wp-content/themes/alpha/libs/constructSocialImage.php?id=<?php echo get_the_ID(); ?>"/><?php
}

J’ai donc réussi à développer un système qui génère une image de manière automatique. Et maintenant, lorsque je partage une URL, cette image est automatiquement créée et affichée dans LinkedIn et en plus, elle est cliquable et renvoie directement vers l’URL de ma page !

05/09/2018

Yann Vangampelaere - nouslesdevs -

Sinon jete un coup d’oeil aux autres catégories

Ma boîte aux lettres

Tu veux me demander quelque chose ?