<?php

/**
 * SPIP, Système de publication pour l'internet
 *
 * Copyright © avec tendresse depuis 2001
 * Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James
 *
 * Ce programme est un logiciel libre distribué sous licence GNU/GPL.
 */

/**
 * Toutes les fonctions image_xx de ce fichier :
 *  - prennent une image en entree
 *  - fournissent une image en sortie
 *  - sont chainables les unes derrieres les autres dans toutes les combinaisons possibles
 *
 * @package SPIP\FiltresImages\ImagesTransforme
 */
if (!defined('_ECRIRE_INC_VERSION')) {
	return;
}

// librairie de base du core
include_spip('inc/filtres_images_mini');

/**
 * Un filtre pour re-orienter automatiquement une image selon son exif - si besoin
 * si pas d'exif ou pas d'orientation, le filtre ne fait rien et renvoie l'image d'origine
 *
 * @param string $src
 * @return string
 */
function image_oriente_selon_exif($src) {

	$fonction = ['image_oriente_selon_exif', func_get_args()];
	$image = _image_valeurs_trans($src, 'oriente_selon_exif', false, $fonction, false, _SVG_SUPPORTED);
	if (!$image) {
		return $src;
	}

	if ($image['creer']) {
		// SVG : on ne fait rien...
		if ($image['format_source'] === 'svg') {
			return $src;
		}

		if (
			(!$orientation = $image['orientation'])
		) {
			return $src;
		}
		include_spip('inc/exif');
		$axe = exif_determiner_axe_symetrie($orientation);
		$angle = exif_determiner_angle_rotation($orientation);
		if ($axe === null && $angle === null) {
			return $src;
		}

		$im = @$image['fonction_imagecreatefrom']($image['fichier']);
		if (!$im) {
			return $src;
		}
		switch ($axe) {
			case 1:
				imageflip($im, IMG_FLIP_HORIZONTAL);
				break;
			case 2:
				imageflip($im, IMG_FLIP_VERTICAL);
				break;
			default:
				break;
		}

		if ($angle) {
			$im = imagerotate($im, $angle, 0);
		}

		// produire le resultat
		_image_gd_output($im, $image);
		if (\PHP_VERSION_ID < 80000) {
			imagedestroy($im);
		}
		unset($im);
	}
	return _image_ecrire_tag($image, ['src' => $image['fichier_dest']]);
}

// 1/ Aplatir une image semi-transparente (supprimer couche alpha)
// en remplissant la transparence avec couleur choisir $coul.
// 2/ Forcer le format de sauvegarde (jpg, png, gif)
// pour le format jpg, $qualite correspond au niveau de compression (defaut 85)
// pour le format gif, $qualite correspond au nombre de couleurs dans la palette (defaut 128)
// pour le format png, $qualite correspond au nombre de couleur dans la palette ou si 0 a une image truecolor (defaut truecolor)
// attention, seul 128 est supporte en l'etat (production d'images avec palette reduite pas satisfaisante)
// 3/ $transparence a "true" permet de conserver la transparence (utile pour conversion GIF)
function image_aplatir($im, $format = 'jpg', $coul = '000000', $qualite = null, $transparence = false) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	$transp_y = null;
	if ($qualite === null) {
		if (in_array($format, ['jpg', 'webp'], true)) {
			$qualite = _IMG_GD_QUALITE;
		} elseif ($format == 'png') {
			$qualite = 0;
		} else {
			$qualite = 128;
		}
	}
	$qualite = intval($qualite);

	$fonction = ['image_aplatir', func_get_args()];
	$image = _image_valeurs_trans($im, "aplatir-$format-$coul-$qualite-$transparence", $format, $fonction, '', _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}
	if ($image['format_source'] === 'svg') {
		// ne pas forcer le format
		$image = _image_valeurs_trans($im, "aplatir-$format-$coul-$qualite-$transparence", false, $fonction, '', _SVG_SUPPORTED);
	}

	include_spip('inc/filtres');
	$couleurs = _couleur_hex_to_dec($coul);
	$dr = $couleurs['red'];
	$dv = $couleurs['green'];
	$db = $couleurs['blue'];

	$x_i = $image['largeur'];
	$y_i = $image['hauteur'];

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];
	if ($creer) {
		if ($image['format_source'] === 'svg') {
			if ($transparence) {
				$svg = svg_charger($im);
			} else {
				$svg = svg_ajouter_background($im, $coul);
			}
			_image_gd_output($svg, $image);
		} else {
			$im = @$image['fonction_imagecreatefrom']($im);
			if (!$im) {
				return '';
			}
			imagepalettetotruecolor($im);

			// si c'est un gif source, commencer par en faire une copie
			if ($image['format_source'] == 'gif') {
				$im_ = imagecreatetruecolor($x_i, $y_i);
				// Si un GIF est transparent,
				// fabriquer un PNG transparent
				// Conserver la transparence
				@imagealphablending($im_, false);
				@imagesavealpha($im_, true);
				if (function_exists('imageAntiAlias')) {
					imageantialias($im_, true);
				}
				@imagecopyresampled($im_, $im, 0, 0, 0, 0, $x_i, $y_i, $x_i, $y_i);
				if (\PHP_VERSION_ID < 80000) {
					imagedestroy($im);
				}
				$im = $im_;
			}

			$im_ = imagecreatetruecolor($x_i, $y_i);
			// allouer la couleur de fond
			if ($transparence) {
				@imagesavealpha($im_, true);
				$color_t = imagecolorallocatealpha($im_, $dr, $dv, $db, 127);
			} else {
				$color_t = imagecolorallocate($im_, $dr, $dv, $db);
			}

			imagefill($im_, 0, 0, $color_t);
			imagelayereffect($im_, IMG_EFFECT_ALPHABLEND);
			imagecopy($im_, $im, 0, 0, 0, 0, $x_i, $y_i);

			// passer en palette si besoin
			if ($format == 'gif' or ($format == 'png' and $qualite !== 0)) {
				// creer l'image finale a palette
				// (on recycle l'image initiale si possible, sinon on en recree une)
				$im = imagecreatetruecolor($x_i, $y_i);

				// utilisons un fond vert en guise de transparence
				$color_t = imagecolorallocate($im, 0, 255, 0);
				imagefill($im, 0, 0, $color_t);
				imagelayereffect($im, IMG_EFFECT_ALPHABLEND);
				@imagecolortransparent($im, $color_t);

				// copier l'image true color
				imagecopy($im, $im_, 0, 0, 0, 0, $x_i, $y_i);
				@imagetruecolortopalette($im, true, $qualite);

				// matcher les couleurs au mieux par rapport a l'image initiale
				// si la fonction est disponible
				if (function_exists('imagecolormatch')) {
					// et la transformer en palette
					@imagecolormatch($im_, $im);
				}

				// produire le resultat
				_image_gd_output($im, $image, $qualite);
				if (\PHP_VERSION_ID < 80000) {
					imagedestroy($im);
				}
				unset($im);
			} else {
				_image_gd_output($im_, $image, $qualite);
			}
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im_);
			}
			unset($im_);
		}
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}

// Enregistrer une image dans un format donne
// (conserve la transparence gif, png, ico)
// utilise [->@image_aplatir]
function image_format($img, $format = 'png') {
	$qualite = null;
	if ($format == 'png8') {
		$format = 'png';
		$qualite = 128;
	}

	return image_aplatir($img, $format, 'cccccc', $qualite, true);
}

// Transforme l'image en PNG transparent
// alpha = 0: aucune transparence
// alpha = 127: completement transparent
function image_alpha($im, $alpha = 63) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_alpha', func_get_args()];
	$image = _image_valeurs_trans($im, "alpha-$alpha", 'png', $fonction, false, _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}
	if ($image['format_source'] === 'svg') {
		// ne pas forcer le format
		$image = _image_valeurs_trans($im, "alpha-$alpha", false, $fonction, '', _SVG_SUPPORTED);
	}

	$x_i = $image['largeur'];
	$y_i = $image['hauteur'];

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		if ($image['format_source'] === 'svg') {
			$atts = [];
			if ($alpha > 0) {
				$atts['opacity'] = round((127 - $alpha) / 127.0, 2);
			}
			$svg = svg_transformer($im, $atts);
			_image_gd_output($svg, $image);
		} else {
			// Creation de l'image en deux temps
			// de facon a conserver les GIF transparents
			$im = $image['fonction_imagecreatefrom']($im);
			imagepalettetotruecolor($im);
			$im2 = imagecreatetruecolor($x_i, $y_i);
			@imagealphablending($im2, false);
			@imagesavealpha($im2, true);
			$color_t = imagecolorallocatealpha($im2, 255, 255, 255, 127);
			imagefill($im2, 0, 0, $color_t);
			imagecopy($im2, $im, 0, 0, 0, 0, $x_i, $y_i);

			$im_ = imagecreatetruecolor($x_i, $y_i);
			imagealphablending($im_, false);
			imagesavealpha($im_, true);

			for ($x = 0; $x < $x_i; $x++) {
				for ($y = 0; $y < $y_i; $y++) {
					$rgb = imagecolorat($im2, $x, $y);

					if (function_exists('imagecolorallocatealpha')) {
						$a = ($rgb >> 24) & 0xFF;
						$r = ($rgb >> 16) & 0xFF;
						$g = ($rgb >> 8) & 0xFF;
						$b = $rgb & 0xFF;

						$a_ = $alpha + $a - round($a * $alpha / 127);
						$rgb = imagecolorallocatealpha($im_, $r, $g, $b, $a_);
					}
					imagesetpixel($im_, $x, $y, $rgb);
				}
			}
			_image_gd_output($im_, $image);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im_);
				imagedestroy($im);
				imagedestroy($im2);
			}
			unset($im_, $im, $im2);
		}
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}

/**
 * Recadre (rogne) une image en indiquant la taille de la découpe souhaitée
 *
 * On peut indiquer une proportion ou une taille spécifique, une position de rognage
 * et une couleur de fond, si le rognage est de taille plus grande que l'image d'origine.
 *
 * @example
 *     - `[(#FICHIER|image_recadre{800, 400})]`
 *     - `[(#FICHIER|image_recadre{800, 400, center})]`
 *     - `[(#FICHIER|image_recadre{800, 400, center, black})]`
 *     - `[(#FICHIER|image_recadre{16:9})]`
 *     - `[(#FICHIER|image_recadre{16:9, -})]` (- est appliqué par défaut, équivalent à image_passe_partout)
 *     - `[(#FICHIER|image_recadre{16:9, +, center, white})]`
 *     - `[(#FICHIER|image_recadre{16:9, -, top left})]`
 *     - `[(#FICHIER|image_recadre{16:9, -, top=40 left=20})]`
 *
 * @filtre
 * @uses _image_valeurs_trans()
 * @uses _image_tag_changer_taille() si image trop grande pour être traitée
 * @uses _image_ecrire_tag()
 * @link https://www.spip.net/5786
 *
 * @param string $im
 *     Chemin de l'image ou balise html `<img src=... />`
 * @param string|int $width
 *     Largeur du recadrage
 *     ou ratio sous la forme "16:9"
 * @param string|int $height
 *     Hauteur du recadrage
 *     ou "+" (agrandir) ou "-" (reduire) si un ratio est fourni pour width
 * @param string $position
 *     Indication de position de la découpe :
 *     - `center`, `left`, `right`, `top`, `bottom`,
 *     - ou combinaisons de plusiers `top left`
 *     - ou indication en pixels depuis une position `top=50` ou composée `top=40 left=50`
 *     - ou nom d'une fonction spéciale qui calculera et retournera la position souhaitée
 * @param string $background_color
 *     Couleur de fond si on agrandit l'image
 * @return string
 *     balise image recadrée
 */
function image_recadre($im, $width, $height = '-', $position = 'center', $background_color = 'white') {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_recadre', func_get_args()];

	$forcer_format = false;
	$background_color = strtolower(trim($background_color));
	$position = strtolower(trim($position));
	$image = _image_valeurs_trans($im, "recadre-$width-$height-$position-$background_color", $forcer_format, $fonction, false, _SVG_SUPPORTED);

	if (!$image) {
		return '';
	}

	$x_i = $image['largeur'];
	$y_i = $image['hauteur'];

	// on recadre pour respecter un ratio ?
	// width : "16:9"
	// height : "+" pour agrandir l'image et "-" pour la croper
	if (str_contains($width, ':')) {
		[$wr, $hr] = explode(':', $width);
		$hm = $x_i / $wr * $hr;
		$ym = $y_i / $hr * $wr;
		if ($height == '+' ? ($y_i < $hm) : ($y_i > $hm)) {
			$width = $x_i;
			$height = round($hm);
		} else {
			$width = round($ym);
			$height = $y_i;
		}
	}

	if ($image['format_source'] !== 'svg' && _IMG_GD_MAX_PIXELS && $x_i * $y_i > _IMG_GD_MAX_PIXELS) {
		spip_log("image_recadre impossible sur $im : " . $x_i * $y_i . 'pixels');

		// on se rabat sur une reduction CSS
		return _image_tag_changer_taille($im, $width, $height);
	}

	if (intval($width) === 0) {
		$width = $x_i;
	}
	if (intval($height) === 0) {
		$height = $y_i;
	}

	$offset_width = $x_i - $width;
	$offset_height = $y_i - $height;

	if ($background_color === 'transparent') {
		if ($width > $x_i or $height > $y_i) {
			if ($image['format_source'] !== 'svg') {
				$forcer_format = 'png';
			}
			$image = _image_valeurs_trans($im, "recadre-$width-$height-$position-$background_color", $forcer_format, $fonction, false, _SVG_SUPPORTED);
		}
	}

	// chercher une fonction spéciale de calcul des coordonnées de positionnement.
	// exemple 'focus' ou 'focus-center' avec le plugin 'Centre Image'
	if (!in_array($position, ['center', 'top', 'right', 'bottom', 'left'], true)) {
		if (count(explode(' ', $position)) == 1) {
			$positionner = charger_fonction('image_positionner_par_' . str_replace('-', '_', $position), 'inc', true);
			if ($positionner) {
				$position = $positionner($im, $width, $height);
			}
		}
	}

	if (str_contains($position, 'left')) {
		if (preg_match(';left=(\d{1}\d+);', $position, $left)) {
			$offset_width = $left[1];
		} else {
			$offset_width = 0;
		}
	} elseif (str_contains($position, 'right')) {
		$offset_width = $offset_width;
	} else {
		$offset_width = intval(ceil($offset_width / 2));
	}

	if (str_contains($position, 'top')) {
		if (preg_match(';top=(\d{1}\d+);', $position, $top)) {
			$offset_height = $top[1];
		} else {
			$offset_height = 0;
		}
	} elseif (str_contains($position, 'bottom')) {
		$offset_height = $offset_height;
	} else {
		$offset_height = intval(ceil($offset_height / 2));
	}

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		if ($image['format_source'] === 'svg') {
			if (!function_exists('svg_recadrer')) {
				include_spip('inc/svg');
			}
			$svg = svg_recadrer($im, $width, $height, $offset_width, $offset_height, $background_color);
			_image_gd_output($svg, $image);
		} else {
			$im = $image['fonction_imagecreatefrom']($im);
			if (!$im) {
				return '';
			}
			imagepalettetotruecolor($im);
			$im_ = imagecreatetruecolor($width, $height);
			@imagealphablending($im_, false);
			@imagesavealpha($im_, true);

			if ($background_color == 'transparent') {
				$color_t = imagecolorallocatealpha($im_, 255, 255, 255, 127);
			} else {
				$bg = _couleur_hex_to_dec($background_color);
				$color_t = imagecolorallocate($im_, $bg['red'], $bg['green'], $bg['blue']);
			}
			imagefill($im_, 0, 0, $color_t);
			imagecopy(
				$im_,
				$im,
				max(0, -$offset_width),
				max(0, -$offset_height),
				max(0, $offset_width),
				max(0, $offset_height),
				min($width, $x_i),
				min($height, $y_i)
			);

			_image_gd_output($im_, $image);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im_);
				imagedestroy($im);
			}
			unset($im_, $im);
		}
	}

	return _image_ecrire_tag($image, ['src' => $dest, 'width' => $width, 'height' => $height]);
}

/**
 * Recadrer une image dans le rectangle le plus petit possible sans perte
 * de pixels non transparent
 * Le recadrage se fait en conservant le centre de l'image : on recadre symétriquement gauche vs droite et haut vs bas
 * TODO : proposer une option pour vraiment recadrer au plus juste, meme si ca decentre l'image
 *
 * @param string $im
 * @return string
 */
function image_recadre_mini($im) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_recadre_mini', func_get_args()];
	$image = _image_valeurs_trans($im, 'recadre_mini', false, $fonction);

	if (!$image) {
		return '';
	}

	$width = $image['largeur'];
	$height = $image['hauteur'];

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];
	if ($creer) {
		$im = $image['fonction_imagecreatefrom']($im);
		imagepalettetotruecolor($im);

		// trouver le rectangle mini qui contient des infos de couleur
		// recherche optimisee qui ne balaye que par zone
		$min_x = $width;
		$min_y = $height;
		$max_y = $max_x = 0;
		$yy = 0;
		while ($yy <= $height / 2 and $max_y <= $min_y) {
			if ($yy < $min_y) {
				for ($xx = 0; $xx < $width; $xx++) {
					$color_index = imagecolorat($im, $xx, $yy);
					$color_tran = imagecolorsforindex($im, $color_index);
					if ($color_tran['alpha'] !== 127) {
						$min_y = min($yy, $min_y);
						$max_y = max($height - 1 - $yy, $max_y);
						break;
					}
				}
			}
			if ($height - 1 - $yy > $max_y) {
				for ($xx = 0; $xx < $width; $xx++) {
					$color_index = imagecolorat($im, $xx, $height - 1 - $yy);
					$color_tran = imagecolorsforindex($im, $color_index);
					if ($color_tran['alpha'] !== 127) {
						$min_y = min($yy, $min_y);
						$max_y = max($height - 1 - $yy, $max_y);
						break;
					}
				}
			}
			$yy++;
		}
		$min_y = min($max_y, $min_y); // tout a 0 aucun pixel trouve

		$xx = 0;
		while ($xx <= $width / 2 and $max_x <= $min_x) {
			if ($xx < $min_x) {
				for ($yy = $min_y; $yy < $max_y; $yy++) {
					$color_index = imagecolorat($im, $xx, $yy);
					$color_tran = imagecolorsforindex($im, $color_index);
					if ($color_tran['alpha'] !== 127) {
						$min_x = min($xx, $min_x);
						$max_x = max($xx, $max_x);
						break; // inutile de continuer sur cette colonne
					}
				}
			}
			if ($width - 1 - $xx > $max_x) {
				for ($yy = $min_y; $yy < $max_y; $yy++) {
					$color_index = imagecolorat($im, $width - 1 - $xx, $yy);
					$color_tran = imagecolorsforindex($im, $color_index);
					if ($color_tran['alpha'] !== 127) {
						$min_x = min($width - 1 - $xx, $min_x);
						$max_x = max($width - 1 - $xx, $max_x);
						break; // inutile de continuer sur cette colonne
					}
				}
			}
			$xx++;
		}
		$min_x = min($max_x, $min_x); // tout a 0 aucun pixel trouve

		$width = $max_x - $min_x + 1;
		$height = $max_y - $min_y + 1;

		$im_ = imagecreatetruecolor($width, $height);
		@imagealphablending($im_, false);
		@imagesavealpha($im_, true);

		$color_t = imagecolorallocatealpha($im_, 255, 255, 255, 127);
		imagefill($im_, 0, 0, $color_t);
		imagecopy($im_, $im, 0, 0, $min_x, $min_y, $width, $height);

		_image_gd_output($im_, $image);
		if (\PHP_VERSION_ID < 80000) {
			imagedestroy($im_);
			imagedestroy($im);
		}
		unset($im_, $im);
	} else {
		[$height, $width] = taille_image($image['fichier_dest']);
	}

	return _image_ecrire_tag($image, ['src' => $dest, 'width' => $width, 'height' => $height]);
}

/**
 * Flip une image selon un axe de symétrie central et vertical
 * (c'est donc un flip horizontal au sens de GD)
 * @param string $im
 * @return string
 */
function image_flip_vertical($im) {
	// reorienter l'image au prealable si besoin, car sinon ça inverse potentiellement le flip
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_flip_vertical', func_get_args()];
	$image = _image_valeurs_trans($im, 'flip_v', false, $fonction, '', _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		if ($image['format_source'] === 'svg') {
			$svg = svg_flip($im, 'v');
			_image_gd_output($svg, $image);
		} else {
			$im = $image['fonction_imagecreatefrom']($im);
			imagepalettetotruecolor($im);
			@imagesavealpha($im, true);
			// historique : la definition de SPIP est l'inverse de celle de GD...
			imageflip($im, IMG_FLIP_HORIZONTAL);
			_image_gd_output($im, $image);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im);
			}
			unset($im);
		}
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}

/**
 * Flip une image selon un axe de symétrie central et horizontal
 * (c'est donc un flip vertical au sens de GD)
 * @param string $im
 * @return string
 */
function image_flip_horizontal($im) {
	// reorienter l'image au prealable si besoin, car sinon ça inverse potentiellement le flip
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_flip_horizontal', func_get_args()];
	$image = _image_valeurs_trans($im, 'flip_h', false, $fonction, '', _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		if ($image['format_source'] === 'svg') {
			$svg = svg_flip($im, 'h');
			_image_gd_output($svg, $image);
		} else {
			$im = $image['fonction_imagecreatefrom']($im);
			imagepalettetotruecolor($im);
			@imagesavealpha($im, true);
			// historique : la definition de SPIP est l'inverse de celle de GD...
			imageflip($im, IMG_FLIP_VERTICAL);
			_image_gd_output($im, $image);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im);
			}
			unset($im);
		}
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}

function image_masque($im, $masque, $pos = '') {
	// reorienter l'image au prealable si besoin, car sinon l'orientation relative du masque et de l'image ne seront pas bonnes
	$im = image_oriente_selon_exif($im);

	$defini = [];
	$rgb3 = [];
	// Passer, en plus de l'image d'origine,
	// une image de "masque": un fichier PNG24 transparent.
	// Le decoupage se fera selon la transparence du "masque",
	// et les couleurs seront eclaircies/foncees selon de couleur du masque.
	// Pour ne pas modifier la couleur, le masque doit etre en gris 50%.
	//
	// Si l'image source est plus grande que le masque, alors cette image est reduite a la taille du masque.
	// Sinon, c'est la taille de l'image source qui est utilisee.
	//
	// $pos est une variable libre, qui permet de passer left=..., right=..., bottom=..., top=...
	// dans ce cas, le masque est place a ces positions sur l'image d'origine,
	// et evidemment cette image d'origine n'est pas redimensionnee
	//
	// Positionnement horizontal: text-align=left, right, center
	// Positionnement vertical : vertical-align=top, bottom, middle
	// (les positionnements left, right, top, left sont relativement inutiles, mais coherence avec CSS)
	//
	// Choix du mode de fusion: mode=masque, normal, eclaircir, obscurcir, produit, difference, ecran, superposer, lumiere_dure, teinte, saturation, valeur
	// https://en.wikipedia.org/wiki/Blend_modes
	// masque: mode par defaut
	// normal: place la nouvelle image par dessus l'ancienne
	// eclaircir: place uniquement les points plus clairs
	// obscurcir: place uniquement les points plus fonc'es
	// produit: multiplie par le masque (points noirs rendent l'image noire, points blancs ne changent rien)
	// difference: remplit avec l'ecart entre les couleurs d'origine et du masque
	// ecran: effet inverse de 'produit' -> l'image resultante est plus claire
	// superposer: combine les modes 'produit' et 'ecran' -> les parties claires sont eclaircies, les parties sombres assombries.
	// lumiere_dure: equivalent a 'superposer', sauf que l'image du bas et du haut sont inversees.
	// teinte: utilise la teinte du masque
	// saturation: utilise la saturation du masque
	// valeur: utilise la valeur du masque

	$mode = 'masque';

	$numargs = func_num_args();
	$arg_list = func_get_args();
	$variable = [];

	$texte = $arg_list[0];
	for ($i = 1; $i < $numargs; $i++) {
		if (($p = strpos($arg_list[$i], '=')) !== false) {
			$nom_variable = substr($arg_list[$i], 0, $p);
			$val_variable = substr($arg_list[$i], $p + 1);
			$variable["$nom_variable"] = $val_variable;
			$defini["$nom_variable"] = 1;
		}
	}
	if (isset($defini['mode']) and $defini['mode']) {
		$mode = $variable['mode'];
	}

	// utiliser _image_valeurs_trans pour accepter comme masque :
	// - une balise <img src='...' />
	// - une image avec un timestamp ?01234
	$mask = _image_valeurs_trans($masque, 'source-image_masque', 'png', null, true);
	if (!$mask || $mask['format_dest'] !== 'png') {
		return '';
	}
	$masque = $mask['fichier'];

	$pos = md5(serialize($variable) . $mask['date_src']);
	$fonction = ['image_masque', func_get_args()];
	$image = _image_valeurs_trans($im, "masque-$masque-$pos", 'png', $fonction);
	if (!$image) {
		return '';
	}

	$x_i = $image['largeur'];
	$y_i = $image['hauteur'];

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	// doit-on positionner l'image ?
	$placer = false;
	foreach (['right', 'left', 'bottom', 'top', 'text-align', 'vertical-align'] as $pl) {
		if (isset($defini[$pl]) and $defini[$pl]) {
			$placer = true;
			break;
		}
	}

	if ($creer) {
		$im_m = $mask['fichier'];
		$x_m = $mask['largeur'];
		$y_m = $mask['hauteur'];

		$im2 = $mask['fonction_imagecreatefrom']($masque);
		if ($mask['format_source'] == 'gif' and function_exists('ImageCopyResampled')) {
			$im2_ = imagecreatetruecolor($x_m, $y_m);
			// Si un GIF est transparent,
			// fabriquer un PNG transparent
			// Conserver la transparence
			if (function_exists('imageAntiAlias')) {
				imageantialias($im2_, true);
			}
			@imagealphablending($im2_, false);
			@imagesavealpha($im2_, true);
			@imagecopyresampled($im2_, $im2, 0, 0, 0, 0, $x_m, $y_m, $x_m, $y_m);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im2);
			}
			$im2 = $im2_;
		}

		if ($placer) {
			// On fabriquer une version "agrandie" du masque,
			// aux dimensions de l'image source
			// et on "installe" le masque dans cette image
			// ainsi: aucun redimensionnement

			$dx = 0;
			$dy = 0;

			if (isset($defini['right']) and $defini['right']) {
				$right = $variable['right'];
				$dx = ($x_i - $x_m) - $right;
			}
			if (isset($defini['bottom']) and $defini['bottom']) {
				$bottom = $variable['bottom'];
				$dy = ($y_i - $y_m) - $bottom;
			}
			if (isset($defini['top']) and $defini['top']) {
				$top = $variable['top'];
				$dy = $top;
			}
			if (isset($defini['left']) and $defini['left']) {
				$left = $variable['left'];
				$dx = $left;
			}
			if (isset($defini['text-align']) and $defini['text-align']) {
				$align = $variable['text-align'];
				if ($align == 'right') {
					$right = 0;
					$dx = ($x_i - $x_m);
				} else {
					if ($align == 'left') {
						$left = 0;
						$dx = 0;
					} else {
						if ($align = 'center') {
							$dx = round(($x_i - $x_m) / 2);
						}
					}
				}
			}
			if (isset($defini['vertical-align']) and $defini['vertical-align']) {
				$valign = $variable['vertical-align'];
				if ($valign == 'bottom') {
					$bottom = 0;
					$dy = ($y_i - $y_m);
				} else {
					if ($valign == 'top') {
						$top = 0;
						$dy = 0;
					} else {
						if ($valign = 'middle') {
							$dy = round(($y_i - $y_m) / 2);
						}
					}
				}
			}

			$im3 = imagecreatetruecolor($x_i, $y_i);
			@imagealphablending($im3, false);
			@imagesavealpha($im3, true);
			if ($mode == 'masque') {
				$color_t = imagecolorallocatealpha($im3, 128, 128, 128, 0);
			} else {
				$color_t = imagecolorallocatealpha($im3, 128, 128, 128, 127);
			}
			imagefill($im3, 0, 0, $color_t);

			imagecopy($im3, $im2, $dx, $dy, 0, 0, $x_m, $y_m);

			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im2);
			}
			$im2 = imagecreatetruecolor($x_i, $y_i);
			@imagealphablending($im2, false);
			@imagesavealpha($im2, true);

			imagecopy($im2, $im3, 0, 0, 0, 0, $x_i, $y_i);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im3);
			}
			unset($im3);
			$x_m = $x_i;
			$y_m = $y_i;
		}

		$rapport = $x_i / $x_m;
		if (($y_i / $y_m) < $rapport) {
			$rapport = $y_i / $y_m;
		}

		$x_d = ceil($x_i / $rapport);
		$y_d = ceil($y_i / $rapport);

		if ($x_i < $x_m or $y_i < $y_m) {
			$x_dest = $x_i;
			$y_dest = $y_i;
			$x_dec = 0;
			$y_dec = 0;
		} else {
			$x_dest = $x_m;
			$y_dest = $y_m;
			$x_dec = round(($x_d - $x_m) / 2);
			$y_dec = round(($y_d - $y_m) / 2);
		}

		$nouveau = _image_valeurs_trans(image_reduire($im, $x_d, $y_d), '');
		if (!is_array($nouveau)) {
			return '';
		}
		$im_n = $nouveau['fichier'];

		$im = $nouveau['fonction_imagecreatefrom']($im_n);
		imagepalettetotruecolor($im);
		if ($nouveau['format_source'] == 'gif' and function_exists('ImageCopyResampled')) {
			$im_ = imagecreatetruecolor($x_dest, $y_dest);
			// Si un GIF est transparent,
			// fabriquer un PNG transparent
			// Conserver la transparence
			if (function_exists('imageAntiAlias')) {
				imageantialias($im_, true);
			}
			@imagealphablending($im_, false);
			@imagesavealpha($im_, true);
			@imagecopyresampled($im_, $im, 0, 0, 0, 0, $x_dest, $y_dest, $x_dest, $y_dest);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im);
			}
			$im = $im_;
		}
		$im_ = imagecreatetruecolor($x_dest, $y_dest);
		@imagealphablending($im_, false);
		@imagesavealpha($im_, true);
		$color_t = imagecolorallocatealpha($im_, 255, 255, 255, 127);
		imagefill($im_, 0, 0, $color_t);

		// calcul couleurs de chaque pixel selon les modes de fusion
		for ($x = 0; $x < $x_dest; $x++) {
			for ($y = 0; $y < $y_dest; $y++) {
				$rgb = imagecolorat($im2, $x, $y); // image au dessus
				$a = ($rgb >> 24) & 0xFF;
				$r = ($rgb >> 16) & 0xFF;
				$g = ($rgb >> 8) & 0xFF;
				$b = $rgb & 0xFF;

				$rgb2 = imagecolorat($im, $x + $x_dec, $y + $y_dec); // image en dessous
				$a2 = ($rgb2 >> 24) & 0xFF;
				$r2 = ($rgb2 >> 16) & 0xFF;
				$g2 = ($rgb2 >> 8) & 0xFF;
				$b2 = $rgb2 & 0xFF;

				if ($mode == 'normal') {
					$v = (127 - $a) / 127;
					if ($v == 1) {
						$r_ = $r;
						$g_ = $g;
						$b_ = $b;
					} else {
						$v2 = (127 - $a2) / 127;
						if ($v + $v2 == 0) {
							$r_ = $r2;
							$g_ = $g2;
							$b_ = $b2;
						} else {
							if ($v2 == 0) {
								$r_ = $r;
								$g_ = $g;
								$b_ = $b;
							} else {
								if ($v == 0) {
									$r_ = $r2;
									$g_ = $g2;
									$b_ = $b2;
								} else {
									$r_ = $r + (($r2 - $r) * $v2 * (1 - $v));
									$g_ = $g + (($g2 - $g) * $v2 * (1 - $v));
									$b_ = $b + (($b2 - $b) * $v2 * (1 - $v));
								}
							}
						}
					}
					$a_ = min($a, $a2);
				} elseif (in_array($mode, ['produit', 'difference', 'superposer', 'lumiere_dure', 'ecran'], true)) {
					if ($mode == 'produit') {
						$r = ($r / 255) * $r2;
						$g = ($g / 255) * $g2;
						$b = ($b / 255) * $b2;
					} elseif ($mode == 'difference') {
						$r = abs($r - $r2);
						$g = abs($g - $g2);
						$b = abs($b - $b2);
					} elseif ($mode == 'superposer') {
						$r = ($r2 < 128) ? 2 * $r * $r2 / 255 : 255 - (2 * (255 - $r) * (255 - $r2) / 255);
						$g = ($g2 < 128) ? 2 * $g * $g2 / 255 : 255 - (2 * (255 - $g) * (255 - $g2) / 255);
						$b = ($b2 < 128) ? 2 * $b * $b2 / 255 : 255 - (2 * (255 - $b) * (255 - $b2) / 255);
					} elseif ($mode == 'lumiere_dure') {
						$r = ($r < 128) ? 2 * $r * $r2 / 255 : 255 - (2 * (255 - $r2) * (255 - $r) / 255);
						$g = ($g < 128) ? 2 * $g * $g2 / 255 : 255 - (2 * (255 - $g2) * (255 - $g) / 255);
						$b = ($b < 128) ? 2 * $b * $b2 / 255 : 255 - (2 * (255 - $b2) * (255 - $b) / 255);
					} elseif ($mode == 'ecran') {
						$r = 255 - (((255 - $r) * (255 - $r2)) / 255);
						$g = 255 - (((255 - $g) * (255 - $g2)) / 255);
						$b = 255 - (((255 - $b) * (255 - $b2)) / 255);
					}
					$r = max(0, min($r, 255));
					$g = max(0, min($g, 255));
					$b = max(0, min($b, 255));

					// melange en fonction de la transparence du masque
					$v = (127 - $a) / 127;
					if ($v == 1) { // melange complet
						$r_ = $r;
						$g_ = $g;
						$b_ = $b;
					} else {
						$v2 = (127 - $a2) / 127;
						if ($v + $v2 == 0) { // ??
							$r_ = $r2;
							$g_ = $g2;
							$b_ = $b2;
						} else { // pas de melange (transparence du masque)
							$r_ = $r + (($r2 - $r) * $v2 * (1 - $v));
							$g_ = $g + (($g2 - $g) * $v2 * (1 - $v));
							$b_ = $b + (($b2 - $b) * $v2 * (1 - $v));
						}
					}
					$a_ = $a2;
				} elseif ($mode == 'eclaircir' or $mode == 'obscurcir') {
					$v = (127 - $a) / 127;
					if ($v == 1) {
						$r_ = $r;
						$g_ = $g;
						$b_ = $b;
					} else {
						$v2 = (127 - $a2) / 127;
						if ($v + $v2 == 0) {
							$r_ = $r2;
							$g_ = $g2;
							$b_ = $b2;
						} else {
							$r_ = $r + (($r2 - $r) * $v2 * (1 - $v));
							$g_ = $g + (($g2 - $g) * $v2 * (1 - $v));
							$b_ = $b + (($b2 - $b) * $v2 * (1 - $v));
						}
					}
					if ($mode == 'eclaircir') {
						$r_ = max($r_, $r2);
						$g_ = max($g_, $g2);
						$b_ = max($b_, $b2);
					} else {
						$r_ = min($r_, $r2);
						$g_ = min($g_, $g2);
						$b_ = min($b_, $b2);
					}
					$a_ = min($a, $a2);
				} elseif (in_array($mode, ['teinte', 'saturation', 'valeur'], true)) {
					include_spip('filtres/images_lib');
					$hsv = _couleur_rgb2hsv($r, $g, $b); // image au dessus
					$h = $hsv['h'];
					$s = $hsv['s'];
					$v = $hsv['v'];
					$hsv2 = _couleur_rgb2hsv($r2, $g2, $b2); // image en dessous
					$h2 = $hsv2['h'];
					$s2 = $hsv2['s'];
					$v2 = $hsv2['v'];
					switch ($mode) {
						case 'teinte':
							$rgb3 = _couleur_hsv2rgb($h, $s2, $v2);
							break;
						case 'saturation':
							$rgb3 = _couleur_hsv2rgb($h2, $s, $v2);
							break;
						case 'valeur':
							$rgb3 = _couleur_hsv2rgb($h2, $s2, $v);
							break;
					}
					$r = $rgb3['r'];
					$g = $rgb3['g'];
					$b = $rgb3['b'];

					// melange en fonction de la transparence du masque
					$v = (127 - $a) / 127;
					if ($v == 1) { // melange complet
						$r_ = $r;
						$g_ = $g;
						$b_ = $b;
					} else {
						$v2 = (127 - $a2) / 127;
						if ($v + $v2 == 0) { // ??
							$r_ = $r2;
							$g_ = $g2;
							$b_ = $b2;
						} else { // pas de melange (transparence du masque)
							$r_ = $r + (($r2 - $r) * $v2 * (1 - $v));
							$g_ = $g + (($g2 - $g) * $v2 * (1 - $v));
							$b_ = $b + (($b2 - $b) * $v2 * (1 - $v));
						}
					}
					$a_ = $a2;
				} else {
					$r_ = $r2 + 1 * ($r - 127);
					$r_ = max(0, min($r_, 255));
					$g_ = $g2 + 1 * ($g - 127);
					$g_ = max(0, min($g_, 255));
					$b_ = $b2 + 1 * ($b - 127);
					$b_ = max(0, min($b_, 255));
					$a_ = $a + $a2 - round($a * $a2 / 127);
				}

				$color = imagecolorallocatealpha($im_, intval($r_), intval($g_), intval($b_), $a_);
				imagesetpixel($im_, $x, $y, $color);
			}
		}

		_image_gd_output($im_, $image);
		if (\PHP_VERSION_ID < 80000) {
			imagedestroy($im_);
			imagedestroy($im);
			imagedestroy($im2);
		}
		unset($im_, $im, $im2);
	}
	$x_dest = largeur($dest);
	$y_dest = hauteur($dest);

	return _image_ecrire_tag($image, ['src' => $dest, 'width' => $x_dest, 'height' => $y_dest]);
}

/**
 * Passage de l'image en noir et blanc
 * un noir & blanc "photo" n'est pas "neutre": les composantes de couleur sont
 * ponderees pour obtenir le niveau de gris
 * on peut ici regler cette ponderation en "pour mille"
 *
 * Par défaut, si on ne passe pas de pondération, on utilise le filtre IMG_FILTER_GRAYSCALE de GD beaucoup plus rapide
 *
 * @param string $im
 * @param ?int $val_r
 * @param ?int $val_g
 * @param ?int $val_b
 * @return string
 */
function image_nb($im, $val_r = null, $val_g = null, $val_b = null) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_nb', func_get_args()];
	$effet = 'nb-' . ($val_r ?? 'default') . '-' . ($val_g ?? 'default') . '-' . ($val_b ?? 'default');
	$image = _image_valeurs_trans($im, $effet, false, $fonction, false, _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}

	$f = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	// Methode precise
	// resultat plus beau, mais tres lourd
	// Et: indispensable pour preserver transparence!

	if ($creer) {
		if ($image['format_source'] === 'svg') {
			# $svg = svg_transformer($f, ['style' => "filter:grayscale(100%);"]); // ne semble pas fonctionner dans Safari+Chrome
			$svg = svg_filter_grayscale($f, 1.0);
			_image_gd_output($svg, $image);
		} else {
			// Creation de l'image en deux temps
			// de facon a conserver les GIF transparents
			$im = $image['fonction_imagecreatefrom']($f);
			imagepalettetotruecolor($im);
			if ($val_r === null && $val_g === null && $val_b === null) {
				spip_log("image_nb: $f via imagefilter", 'images' . _LOG_DEBUG);
				@imagesavealpha($im, true);
				imagefilter($im, IMG_FILTER_GRAYSCALE);
				_image_gd_output($im, $image);
				if (\PHP_VERSION_ID < 80000) {
					imagedestroy($im);
				}
				unset($im);
			} else {
				spip_log("image_nb: $f via calcul manuel", 'images' . _LOG_DEBUG);
				// calcul à la main, beaucoup plus lent
				$default = ['r' => 299, 'g' => 587, 'b' => 114];
				$val_r ??= $default['r'];
				$val_g ??= $default['g'];
				$val_b ??= $default['b'];

				$x_i = $image['largeur'];
				$y_i = $image['hauteur'];

				$im_ = imagecreatetruecolor($x_i, $y_i);
				@imagealphablending($im_, false);
				@imagesavealpha($im_, true);
				$color_t = imagecolorallocatealpha($im_, 255, 255, 255, 127);
				imagefill($im_, 0, 0, $color_t);
				imagecopy($im_, $im, 0, 0, 0, 0, $x_i, $y_i);

				for ($x = 0; $x < $x_i; $x++) {
					for ($y = 0; $y < $y_i; $y++) {
						$rgb = imagecolorat($im_, $x, $y);
						$a = ($rgb >> 24) & 0xFF;
						$r = ($rgb >> 16) & 0xFF;
						$g = ($rgb >> 8) & 0xFF;
						$b = $rgb & 0xFF;

						$c = round(($val_r * $r / 1000) + ($val_g * $g / 1000) + ($val_b * $b / 1000));
						if ($c < 0) {
							$c = 0;
						}
						if ($c > 254) {
							$c = 254;
						}

						$color = imagecolorallocatealpha($im_, $c, $c, $c, $a);
						imagesetpixel($im_, $x, $y, $color);
					}
				}
				_image_gd_output($im_, $image);
				if (\PHP_VERSION_ID < 80000) {
					imagedestroy($im_);
					imagedestroy($im);
				}
				unset($im_, $im);
			}
		}
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}

/**
 * Calcul d'un flou sur une image.
 * On utilise une matrice de convolution pour le niveau 1 et le niveau 2, qu'on repasse autant de fois que nécessaire
 * pour arriver au niveau de flou souhaité qui est appriximativement le rayon du flou
 * Le filtre préserve la transparence
 *
 * @param int $niveau
 * @return string
 */
function image_flou($im, $niveau = 3) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_flou', func_get_args()];
	$image = _image_valeurs_trans($im, "flou-$niveau", false, $fonction, '', _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}

	$x_i = $image['largeur'];
	$y_i = $image['hauteur'];

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	$new_width = $x_i + $niveau;
	$new_height = $y_i + $niveau;
	if ($creer) {
		spip_timer('image_flou');
		if ($image['format_source'] === 'svg') {
			spip_log($log = "image_flou: $im via SVG", 'images' . _LOG_DEBUG);
			$svg = svg_recadrer($im, $new_width, $new_height, round(($x_i - $new_width) / 2), round(($y_i - $new_height) / 2));
			# $svg = svg_transformer($svg, ['style' => "filter:blur({$niveau}px);"]); // ne semble pas supporte par safari+chrome
			$svg = svg_filter_blur($svg, $niveau);
			_image_gd_output($svg, $image);
		} else {
			spip_log($log = "image_flou: $im via gd legacy", 'images' . _LOG_DEBUG);
			// Creation de l'image en deux temps
			// de facon a conserver les GIF transparents
			$im = $image['fonction_imagecreatefrom']($im);
			imagepalettetotruecolor($im);
			if ($niveau > 0) {
				// preparer l'image agrandie
				$temp = imagecreatetruecolor($new_width, $new_height);
				imageantialias($temp, true);
				@imagesavealpha($temp, true);
				// les pixels en transparence 127 sont pris pour du noir par le imageconvolution de GD
				$color_t = imagecolorallocatealpha($temp, 255, 255, 255, 126);
				imagefill($temp, 0, 0, $color_t);
				@imagealphablending($temp, true);
				@imagecopy($temp, $im, ($new_width - $x_i) / 2, ($new_height - $y_i) / 2, 0, 0, $x_i, $y_i);
				if (\PHP_VERSION_ID < 80000) {
					imagedestroy($im);
				}
				unset($im);

				$gaussians = [
					1 => [[1.0, 1.0, 0.0], [2.0, 2.0, 0.0], [0.0, 0.0, 0.0]],
					2 => [[1.0, 2.0, 1.0], [2.0, 4.0, 2.0], [1.0, 2.0, 1.0]],
				];
				$divisors = [];
				foreach ($gaussians as $k => $v) {
					$divisors[$k] = array_sum(array_map('array_sum', $gaussians[$k]));
				}
				while ($niveau > 0) {
					$step = min($niveau, 2);
					imageconvolution($temp, $gaussians[$step], $divisors[$step], 0);
					$niveau -= $step;
				}
				_image_gd_output($temp, $image);
				if (\PHP_VERSION_ID < 80000) {
					imagedestroy($temp);
				}
				unset($temp);
			} else {
				_image_gd_output($im, $image);
				if (\PHP_VERSION_ID < 80000) {
					imagedestroy($im);
				}
				unset($im);
			}
		}
		spip_log($log . ' en ' . spip_timer('image_flou'), 'images' . _LOG_DEBUG);
	}

	return _image_ecrire_tag($image, ['src' => $dest, 'width' => $new_width, 'height' => $new_height]);
}

/**
 * Determiner les nouvelles dimensions de l'image apres rotation
 * @param int|float $angle
 *   en degres
 * @param int $width
 * @param int $height
 * @param int $center_x
 * @param int $center_y
 * @return array
 *  [int, int]
 */
function dimensions_rotation_image($angle, $width, $height, $center_x = null, $center_y = null) {
	// calculer dimensions en simplifiant angles droits, ce qui evite "floutage"
	// des rotations a angle droit
	if (round($angle / 90) * 90 == $angle) {
		$droit = true;
		if (round($angle / 180) * 180 == $angle) {
			$rotate_height = $height;
			$rotate_width = $width;
		} else {
			$rotate_height = $width;
			$rotate_width = $height;
			$rot = 90;
		}
	} else {
		$droit = false;

		// convert degrees to radians
		$angle += 180;
		$angle = deg2rad($angle);

		if ($center_x === null) {
			$center_x = floor(($width - 1) / 2);
		}
		if ($center_y === null) {
			$center_y = floor(($height - 1) / 2);
		}

		$cosangle = cos($angle);
		$sinangle = sin($angle);

		$corners = [[0, 0], [$width, 0], [$width, $height], [0, $height]];

		$corners = array_map(function ($v) use ($center_x, $center_y, $cosangle, $sinangle) {
			// Translate coords to center for rotation
			$v[0] -= $center_x;
			$v[1] -= $center_y;
			return [
				$v[0] * $cosangle + $v[1] * $sinangle,
				$v[1] * $cosangle - $v[0] * $sinangle,
			];
		}, $corners);

		$min_x = min(array_column($corners, 0));
		$max_x = max(array_column($corners, 0));
		$min_y = min(array_column($corners, 1));
		$max_y = max(array_column($corners, 1));

		$rotate_width = ceil($max_x - $min_x);
		$rotate_height = ceil($max_y - $min_y);
	}

	return [$rotate_width, $rotate_height, $droit];
}

/**
 * permet de faire tourner une image d'un angle quelconque
 * le flag "crop" permet de recadrer l'image pour conserver sa taille d'origine
 *
 * @param string $im
 * @param float $angle
 * @param bool $crop
 * @return string
 */
function image_rotation($im, $angle, $crop = false) {
	// reorienter l'image au prealable si besoin, car sinon on a pas la bonne base de rotation
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_rotation', func_get_args()];
	$image = _image_valeurs_trans($im, "rot-$angle", 'png', $fonction, false, _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}
	if ($image['format_source'] === 'svg') {
		// ne pas forcer le format
		$image = _image_valeurs_trans($im, "rot-$angle", false, $fonction, false, _SVG_SUPPORTED);
	}

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		spip_timer('image_rotation');
		if ($image['format_source'] === 'svg') {
			$center_x = floor(($image['largeur'] - 1) / 2);
			$center_y = floor(($image['hauteur'] - 1) / 2);
			[$rotate_width, $rotate_height, $droit] = dimensions_rotation_image(
				$angle,
				$image['largeur'],
				$image['hauteur'],
				$center_x,
				$center_y
			);
			$svg = svg_recadrer(
				$im,
				$rotate_width,
				$rotate_height,
				-round(($rotate_width - $image['largeur']) / 2),
				-round(($rotate_height - $image['hauteur']) / 2)
			);
			spip_log($log = "image_rotation: $im avec SVG", 'images' . _LOG_DEBUG);
			$svg = svg_rotate($svg, $angle, 0.5, 0.5);
			_image_gd_output($svg, $image);
		} else {
			$effectuer_gd = true;

			if ($GLOBALS['meta']['image_process'] == 'convert') {
				if (!defined('_CONVERT_COMMAND')) {
					@define('_CONVERT_COMMAND', 'convert');
					@define('_ROTATE_COMMAND', _CONVERT_COMMAND . ' -background none -rotate %t %src %dest');
				} else {
					@define('_ROTATE_COMMAND', '');
				}
				if (_ROTATE_COMMAND !== '') {
					spip_log($log = "image_rotation: $im avec convert", 'images' . _LOG_DEBUG);
					$commande = str_replace(
						['%t', '%src', '%dest'],
						[$angle, escapeshellcmd($im), escapeshellcmd($dest)],
						_ROTATE_COMMAND
					);
					spip_log($commande, 'images' . _LOG_DEBUG);
					exec($commande);
					if (file_exists($dest)) { // precaution
						$effectuer_gd = false;
					}
				}
			}
			if ($effectuer_gd) {
				spip_log($log = "image_rotation: $im avec gd", 'images' . _LOG_DEBUG);
				// Creation de l'image en deux temps
				// de facon a conserver les GIF transparents
				$im = $image['fonction_imagecreatefrom']($im);
				imagepalettetotruecolor($im);
				$transparent = imagecolorallocatealpha($im, 0, 0, 0, 127);
				$im = imagerotate($im, 360 - $angle, $transparent);
				_image_gd_output($im, $image);
				if (\PHP_VERSION_ID < 80000) {
					imagedestroy($im);
				}
				unset($im);
			}
		}
		$t = spip_timer('image_rotation');
		spip_log("$log en $t", 'images' . _LOG_DEBUG);
	}
	[$src_y, $src_x] = taille_image($dest);

	$image_tournee = _image_ecrire_tag($image, ['src' => $dest, 'width' => $src_x, 'height' => $src_y]);
	if ($crop) {
		$image_tournee = image_recadre($image_tournee, $image['largeur'], $image['hauteur'], 'center', 'transparent');
	}
	return $image_tournee;
}

/**
 * Permet d'appliquer un filtre basé sur une méthode Imagick:: a une image
 * par exemple: [(#LOGO_ARTICLE|image_imagick{waveImage,20,60})]
 * Les arguments suivant le nom de la méthode sont ceux de la méthode
 *
 * Liste des méthodes: https://www.php.net/manual/en/book.imagick.php
 *
 * @return string
 * @throws ImagickException
 *
 * @uses _image_imagick_write()
 */
function image_imagick() {
	$arr = [];
	$tous = func_get_args();
	$img = $tous[0];
	$fonc = $tous[1];
	$tous[0] = '';
	$tous_var = implode('-', $tous);

	// reorienter l'image au prealable si besoin
	$img = image_oriente_selon_exif($img);

	$fonction = ['image_imagick', func_get_args()];
	$image = _image_valeurs_trans($img, "$tous_var", 'png', $fonction);
	if (!$image) {
		return '';
	}

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		if (method_exists(\Imagick::class, $fonc)) {
			$imagick = new Imagick();
			$imagick->readImage($im);
			$args = $tous;
			array_shift($args);
			array_shift($args);
			call_user_func_array([$imagick, $fonc], $args);
			_image_gd_output($imagick, $image, _IMG_GD_QUALITE, '_image_imagick_write');
		} else {
			erreur_squelette(_L("Methode Imagick::{$fonc} inconnue"));
			return '';
		}
	}
	[$src_y, $src_x] = taille_image($dest);

	return _image_ecrire_tag($image, ['src' => $dest, 'width' => $src_x, 'height' => $src_y]);
}

/**
 * Ecriture sur le disque d'une umage générée par Imagick
 *
 * @see image_imagick()
 *
 * @param string $fichier
 *     Le path vers l'image (ex : local/cache-vignettes/L180xH51/image.png).
 * @return bool
 *
 *     - false si l'image créée a une largeur nulle ou n'existe pas ;
 *     - true si une image est bien retournée.
 */
function _image_imagick_write(\Imagick $imagick, $fichier, $qualite = _IMG_GD_QUALITE) {
	$tmp = $fichier . '.tmp';
	$ret = $imagick->writeImage($tmp);

	if (file_exists($tmp)) {
		$taille_test = getimagesize($tmp);
		if ($taille_test[0] < 1) {
			return false;
		}

		spip_unlink($fichier); // le fichier peut deja exister
		@rename($tmp, $fichier);

		return $ret;
	}

	return false;
}

// Permet de rendre une image
// plus claire (gamma > 0)
// ou plus foncee (gamma < 0)
function image_gamma($im, $gamma = 0) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	include_spip('filtres/images_lib');
	$fonction = ['image_gamma', func_get_args()];
	$image = _image_valeurs_trans($im, "gamma-$gamma", false, $fonction, false, _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}

	$x_i = $image['largeur'];
	$y_i = $image['hauteur'];

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		if ($image['format_source'] === 'svg') {
			$pc = round($gamma / 255, 2);
			if ($pc > 0) {
				$svg = svg_ajouter_voile($im, '#ffffff', $pc);
			} else {
				$svg = svg_ajouter_voile($im, '#000000', -$pc);
			}
			_image_gd_output($svg, $image);
		} else {
			// Creation de l'image en deux temps
			// de facon a conserver les GIF transparents
			$im = $image['fonction_imagecreatefrom']($im);
			imagepalettetotruecolor($im);
			@imagesavealpha($im, true);
			imagefilter($im, IMG_FILTER_BRIGHTNESS, $gamma);
			_image_gd_output($im, $image);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im);
			}
			unset($im);
		}
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}

/**
 * Passe l'image en "sepia"
 * On peut fixer les valeurs RGB
 * de la couleur "complementaire" pour forcer une dominante
 *
 * Par defaut, on utilise les fonciton de gd pour un résultat plus rapide
 * mais avec un peu moins de piquant que la méthode historique de SPIP,
 * dont on peut retrouver le résultat en passant $legacy = true en argument
 *
 * @param string $im
 * @param string $rgb
 * @param bool $legacy
 * @return mixed|string
 */
function image_sepia($im, $rgb = '896f5e', $legacy = false) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	include_spip('filtres/images_lib');

	if (!function_exists('imagecreatetruecolor')) {
		return $im;
	}

	$couleurs = _couleur_hex_to_dec($rgb);
	$dr = $couleurs['red'];
	$dv = $couleurs['green'];
	$db = $couleurs['blue'];

	$fonction = ['image_sepia', func_get_args()];
	$image = _image_valeurs_trans($im, "sepia-$dr-$dv-$db-" . ($legacy ? 'legacy' : 'gd'), false, $fonction, false, _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		spip_timer('sepia');
		$log = "image_sepia: caldul de $im par methode ";
		if ($image['format_source'] === 'svg') {
			$log .= 'svg';
			include_spip('inc/svg');
			# $svg = svg_transformer($im, ['style' => "filter:sepia(1);"]); // ne fonctionne pas dans Chrome+Safari
			$svg = svg_filter_sepia($im, 1);
			_image_gd_output($svg, $image);
		} elseif (!$legacy) {
			$log .= 'gd';
			$im = $image['fonction_imagecreatefrom']($im);
			imagepalettetotruecolor($im);
			@imagealphablending($im, false);
			@imagesavealpha($im, true);
			$x_i = $image['largeur'];
			$y_i = $image['hauteur'];
			$im_ = imagecreatetruecolor($x_i, $y_i);
			@imagealphablending($im_, true);
			@imagesavealpha($im_, true);
			@imagecopy($im_, $im, 0, 0, 0, 0, $x_i, $y_i);
			imagefilter($im_, IMG_FILTER_GRAYSCALE);

			$k = 0.2;
			imagefilter($im, IMG_FILTER_COLORIZE, round($k * ($dr - 127)), round($k * ($dv - 127)), round($k * ($db - 127)));
			imagefilter($im, IMG_FILTER_GRAYSCALE);

			$kr = 0.75;
			$ddr = -40;
			$kv = 0.75;
			$ddv = -40;
			$kb = 0.85;
			$ddb = -40;
			imagefilter($im, IMG_FILTER_COLORIZE, round($kr * ($dr - 127)) + $ddr, round($kv * ($dv - 127)) + $ddv, round($kb * ($db - 127)) + $ddb);
			imagelayereffect($im, IMG_EFFECT_OVERLAY);
			@imagecopy($im, $im_, 0, 0, 0, 0, $x_i, $y_i);
			imagelayereffect($im, IMG_EFFECT_NORMAL);
			imagefilter($im, IMG_FILTER_CONTRAST, 3);
			imagefilter($im, IMG_FILTER_BRIGHTNESS, 7);

			_image_gd_output($im, $image);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im);
			}
			unset($im);
		} else {
			$log .= 'legacy';
			$x_i = $image['largeur'];
			$y_i = $image['hauteur'];

			// Creation de l'image en deux temps
			// de facon a conserver les GIF transparents
			$im = $image['fonction_imagecreatefrom']($im);
			imagepalettetotruecolor($im);
			$im_ = imagecreatetruecolor($x_i, $y_i);
			@imagealphablending($im_, false);
			@imagesavealpha($im_, true);
			$color_t = imagecolorallocatealpha($im_, 255, 255, 255, 127);
			imagefill($im_, 0, 0, $color_t);
			imagecopy($im_, $im, 0, 0, 0, 0, $x_i, $y_i);

			for ($x = 0; $x < $x_i; $x++) {
				for ($y = 0; $y < $y_i; $y++) {
					$rgb = imagecolorat($im_, $x, $y);
					$a = ($rgb >> 24) & 0xFF;
					$r = ($rgb >> 16) & 0xFF;
					$g = ($rgb >> 8) & 0xFF;
					$b = $rgb & 0xFF;

					$gray = (.299 * $r + .587 * $g + .114 * $b);
					$k1 = ($gray / 127.5 - 1.0);
					if ($gray <= 127.5) {
						$r = round($dr * $k1) + $dr;
						$g = round($dv * $k1) + $dv;
						$b = round($db * $k1) + $db;
					} else {
						$k2 = 255 * $k1;
						$r = round($k2 - $dr * $k1) + $dr;
						$g = round($k2 - $dv * $k1) + $dv;
						$b = round($k2 - $db * $k1) + $db;
					}
					$color = imagecolorallocatealpha($im_, $r, $g, $b, $a);
					imagesetpixel($im_, $x, $y, $color);
				}
			}
			_image_gd_output($im_, $image);
			if (\PHP_VERSION_ID < 80000) {
				imagedestroy($im_);
				imagedestroy($im);
			}
			unset($im_, $im);
		}
		spip_log("$log, calcul en " . spip_timer('sepia'), 'images' . _LOG_DEBUG);
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}

/**
 * Renforcer la netteté d'une image
 *
 * @param string $im
 *     Code HTML de l'image
 * @param float $k
 *     Niveau de renforcement (entre 0 et 1)
 * @return string Code HTML de l'image
 */
function image_renforcement($im, $k = 0.5) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_renforcement', func_get_args()];
	$image = _image_valeurs_trans($im, "renforcement-$k", false, $fonction);
	if (!$image) {
		return '';
	}

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];
	$creer = $image['creer'];

	if ($creer) {
		spip_timer('image_renforcement');
		spip_log($log = "image_renforcement: $im via gd legacy", 'images' . _LOG_DEBUG);

		$im = $image['fonction_imagecreatefrom']($im);
		imagepalettetotruecolor($im);
		@imagealphablending($im, true);
		@imagesavealpha($im, true);

		$sharpen = [
			[0.0, -$k, 0.0],
			[-$k, 1 + 4 * $k, -$k],
			[0.0, -$k, 0.0],
		];
		imageconvolution($im, $sharpen, 1, 0);
		_image_gd_output($im, $image);

		spip_log($log . ' en ' . spip_timer('image_renforcement'), 'images' . _LOG_DEBUG);
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}

/**
 * Transforme la couleur de fond de l'image en transparence
 * Le filtre ne gere pas la notion de contiguite aux bords, et affectera tous les pixels de l'image dans la couleur visee
 * $background_color : couleur cible
 * $tolerance : distance L1 dans l'espace RGB des couleur autour de la couleur $background_color pour lequel la transparence sera appliquee
 * $alpha : transparence a appliquer pour les pixels de la couleur cibles avec la tolerance ci-dessus
 * $coeff_lissage : coeff applique a la tolerance pour determiner la decroissance de la transparence fonction de la distance L1 entre la couleur du pixel et la couleur cible
 *
 * @param string $im
 * @param string $background_color
 * @param int $tolerance
 * @param int $alpha
 *   alpha = 0: aucune transparence
 *   alpha = 127: completement transparent
 * @param int $coeff_lissage
 * @return mixed|null|string
 */
function image_fond_transparent($im, $background_color, $tolerance = 12, $alpha = 127, $coeff_lissage = 7) {
	// reorienter l'image au prealable si besoin
	$im = image_oriente_selon_exif($im);

	$fonction = ['image_fond_transparent', func_get_args()];
	$image = _image_valeurs_trans($im, "fond_transparent-$background_color-$tolerance-$coeff_lissage-$alpha", 'png', $fonction, false, _SVG_SUPPORTED);
	if (!$image) {
		return '';
	}
	if ($image['format_source'] === 'svg') {
		// Ne rien faire si SVG
		$image = _image_valeurs_trans($im, "fond_transparent-$background_color-$tolerance-$coeff_lissage-$alpha", false, $fonction);
	}

	$x_i = $image['largeur'];
	$y_i = $image['hauteur'];

	$im = $image['fichier'];
	$dest = $image['fichier_dest'];

	$creer = $image['creer'];

	if ($creer) {
		$bg = _couleur_hex_to_dec($background_color);
		$bg_r = $bg['red'];
		$bg_g = $bg['green'];
		$bg_b = $bg['blue'];

		// Creation de l'image en deux temps
		// de facon a conserver les GIF transparents
		$im = $image['fonction_imagecreatefrom']($im);
		imagepalettetotruecolor($im);
		$im2 = imagecreatetruecolor($x_i, $y_i);
		@imagealphablending($im2, false);
		@imagesavealpha($im2, true);
		$color_t = imagecolorallocatealpha($im2, 255, 255, 255, 127);
		imagefill($im2, 0, 0, $color_t);
		imagecopy($im2, $im, 0, 0, 0, 0, $x_i, $y_i);

		$im_ = imagecreatetruecolor($x_i, $y_i);
		imagealphablending($im_, false);
		imagesavealpha($im_, true);
		$color_f = imagecolorallocatealpha($im_, 255, 255, 255, $alpha);

		for ($x = 0; $x < $x_i; $x++) {
			for ($y = 0; $y < $y_i; $y++) {
				$rgb = imagecolorat($im2, $x, $y);
				$r = ($rgb >> 16) & 0xFF;
				$g = ($rgb >> 8) & 0xFF;
				$b = $rgb & 0xFF;
				if ((($d = abs($r - $bg_r) + abs($g - $bg_g) + abs($b - $bg_b)) <= $tolerance)) {
					imagesetpixel($im_, $x, $y, $color_f);
				} elseif ($tolerance and $d <= ($coeff_lissage + 1) * $tolerance) {
					$transp = round($alpha * (1 - ($d - $tolerance) / ($coeff_lissage * $tolerance)));
					$color_p = imagecolorallocatealpha($im_, $r, $g, $b, $transp);
					imagesetpixel($im_, $x, $y, $color_p);
				} else {
					imagesetpixel($im_, $x, $y, $rgb);
				}
			}
		}
		_image_gd_output($im_, $image);
		if (\PHP_VERSION_ID < 80000) {
			imagedestroy($im_);
			imagedestroy($im);
			imagedestroy($im2);
		}
		unset($im_, $im, $im2);
	}

	return _image_ecrire_tag($image, ['src' => $dest]);
}
