如何使用PHP GD库生成海报
虽然PHP生成海报速度很慢而且各种编码问题十分让人头疼,但还是经常碰到需要生成海报的需求。
官网介绍
GD库有着丰富的方法供开发者调用,详细在PHP官网都能查询到。 GD 和图像处理 函数
下载&加载图片
官网提供了一系列方法加载各种格式图片,但我比较倾向直接用图片内容去加载,因为有时图片资源可能是网络资源直接获取偶尔会失败。
PHP获取图片内容可以用 curl 或者 file_get_contents()
测试了两种方法感觉curl获取网络资源比较合适,本地资源使用 file_get_contents 获取。
这里我简单封装了一下CURL的操作。
function getCurl($url) { $ch = curl_init(); if (stripos($url, 'https://') !== false) { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); } curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, false); $content = curl_exec($ch); curl_close($ch); return $content; }
可以获取图片内容后就可以将图片内容加载成GD资源,如下的代码是下载图片并加载图片到GD资源再保存到本地。
$imgUrl = 'http://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKjMm8BLuicTicOTXCe2gLo0LoYbpxhWQtwSeIQ8NU1icbrnUBHHr9Iss8dYAuIHiaEZXSenPqRGgAcgQ/132'; // 获取图片内容 $imgContent = getCurl($imgUrl); // 加载到图片资源 $imageSource = imagecreatefromstring($imgContent); // 将图片资源保存成图片 $path = './head.jpg'; imagejpeg($imageSource, $path, 90); // 销毁图片资源 imagedestroy($imageSource);
叠加图片
海报生成直接拿GD绘画的话难度很高,所以可以将一层一层画好的不同图片叠在一起来完成海报。
这里尝试生成一张灰色的背景并将头像按不同大小合成在背景上。
主要用到 imagecopyresampled 方法来叠加两张图片,官网有很详细的说明。
// 灰色背景的尺寸 $boardWidth = 1080; $boardHeight = 1080; // 生成指定尺寸的图象 $boardSource = imagecreatetruecolor($boardWidth, $boardHeight); // 图像分配个灰色,后面的参数是GRB $gray = imagecolorallocate($boardSource, 222, 222, 222); // 填充颜色,此时真正得到1080 * 1080 尺寸的灰色背景 imagefill($boardSource, 0, 0, $gray); $imgUrl = 'http://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKjMm8BLuicTicOTXCe2gLo0LoYbpxhWQtwSeIQ8NU1icbrnUBHHr9Iss8dYAuIHiaEZXSenPqRGgAcgQ/132'; // 获取头像内容 $imgContent = getCurl($imgUrl); // 加载到图象资源 $imageSource = imagecreatefromstring($imgContent); // 获取图象资源尺寸 list($imageWidth, $imageHeight) = getimagesizefromstring($imgContent); // 将头像图片放到背景图片上 // 这里参数第一个是目标图象,第二个是源图象 这里分别是背景和头像 // 中间四个分别是坐标位置 // 最后四个分别是 目标的宽高 与 源图象的宽高 // 这里相当于把头像按不同大小合成在背景图上 imagecopyresampled($boardSource, $imageSource, 0, 0, 0, 0, $imageWidth / 2, $imageHeight / 2, $imageWidth, $imageHeight); imagecopyresampled($boardSource, $imageSource, 150, 150, 0, 0, $imageWidth, $imageHeight, $imageWidth, $imageHeight); imagecopyresampled($boardSource, $imageSource, 400, 400, 0, 0, $imageWidth * 2, $imageHeight * 2, $imageWidth, $imageHeight); // 将图片资源保存成图片 $path = './composite_picture.jpg'; imagejpeg($boardSource, $path, 90); // 销毁图片资源 imagedestroy($boardSource); imagedestroy($imageSource);
执行上面代码生成的图片是这样的:
文字处理
生成海报很有可能需要在图片上填写文字,如果不特意处理的话很可能生成的是乱码。
这里测试一下常见情况生成的结果:
// 背景的尺寸 $boardWidth = 400; $boardHeight = 400; // 生成指定尺寸的图象 $boardSource = imagecreatetruecolor($boardWidth, $boardHeight); // 图像分配个白色,后面的参数是GRB $white = imagecolorallocate($boardSource, 255, 255, 255); // 给背景填充颜色 imagefill($boardSource, 0, 0, $white);
//普通中文文字 $text = '测试编码问题'; //测试符号问题 $pictogram = '¥';
//测试emoji表情问题
$emoji = '??';
// 自定义字体这里用绝对路径,我试着用相对路径没有生效。
$normalFontPath = 'E:\php\test\poster\SourceHanSansSC-Normal.otf';
// 字体大小
$fontSize = 39; // 生成个颜色给字体
$black = imagecolorallocate($boardSource, 38, 38, 38);
// 普通中文
imagettftext($boardSource, 39, 0, 10, 100, $black, $normalFontPath, $text);
// 符号
imagettftext($boardSource, 39, 0, 10, 190, $black, $normalFontPath, $pictogram);
// emoji
imagettftext($boardSource, 39, 0, 10, 280, $black, $normalFontPath, $emoji);
// 保存成图片
$path = './text.jpg'; imagejpeg($boardSource, $path, 90);
// 销毁资源
imagedestroy($boardSource);
生成的图片是这样的:
很幸运的是前两种在我本地是可以渲染的,但是在实际使用时候我碰过连普通中文都不能正常显示 /(ㄒoㄒ)/~~。
现在要做的就是处理这些问题,前两种思路是转换成 html-entities 编码。第三种就绝了要转成图片然后再贴上去??。
前两种可以使用PHP编码转换方法或者自己写的转换方法(根据ASCII值来判断):
$text = mb_convert_encoding($text, "html-entities", "utf-8");
function toEntities($string) { $len = strlen($string); $buf = ""; for ($i = 0; $i < $len; $i++) { if (ord($string[ $i ]) <= 127) { $buf .= $string[ $i ]; } else if (ord($string[ $i ]) < 192) { //unexpected 2nd, 3rd or 4th byte $buf .= "�"; } else if (ord($string[ $i ]) < 224) { //first byte of 2-byte seq $buf .= sprintf("%d;", ((ord($string[ $i + 0 ]) & 31) << 6) + (ord($string[ $i + 1 ]) & 63)); $i += 1; } else if (ord($string[ $i ]) < 240) { //first byte of 3-byte seq $buf .= sprintf("%d;", ((ord($string[ $i + 0 ]) & 15) << 12) + ((ord($string[ $i + 1 ]) & 63) << 6) + (ord($string[ $i + 2 ]) & 63)); $i += 2; } else { //first byte of 4-byte seq $buf .= sprintf("%d;", ((ord($string[ $i + 0 ]) & 7) << 18) + ((ord($string[ $i + 1 ]) & 63) << 12) + ((ord($string[ $i + 2 ]) & 63) << 6) + (ord($string[ $i + 3 ]) & 63)); $i += 3; } } return $buf; }
第三种要做的事件就比较多了并且需要一个emoji库,这里我把emoji库上传到网盘方便使用。
以下是生成emoji表情的代码:
链接:https://pan.baidu.com/s/1j88xpC4UC44iG1G9rY82cQ
提取码:netr
function getEmojiImg($emoji, int $emojiSize, $emojiColor, $emojiFont, int $emojiWidth, int $emojiHeight, int $emojiLength = 7, $color = '') { // 背景 $backgroundSource = imagecreatetruecolor($emojiWidth, $emojiHeight); if (empty($color)) { $color = imagecolorallocate($backgroundSource, 255, 255, 255); } imagefill($backgroundSource, 0, 0, $color); $xWidth = 0; $yTextHeight = $emojiHeight * 3 / 4; $yEmojiHeight = $emojiHeight * 1 / 6; $wordSpace = 20; $emojiWordSpace = 39; $tinyEmojiWidth = 39; $tinyEmojiHeight = 39; // emoji 生成图片保存的临时路径 $emojiFullPath = 'E:\php\test\poster\\'; if (!is_dir($emojiFullPath)) { mkdir($emojiFullPath, 0755, true); } // 超过最大字数处理 $nameLength = mb_strlen($emoji); if ($nameLength > $emojiLength) { $emoji = mb_substr($emoji, 0, $emojiLength) . '...'; } // emoji 处理 $hasEmoji = haveEmoji($emoji); if ($hasEmoji) { //有emoji表情 $emojiWords = splitEmojiInStr($emoji); // 这里要换成自己保存库的路径 $emojiLib = json_decode(file_get_contents('E:\php\test\poster\emoji_image_lib')); foreach ($emojiWords as $word) { $wordType = $word[0]; if ($wordType == 'emoji') {//处理emoji表情,匹配到图片就贴图,匹配不到就过滤表情 $matchLibImg = false; $emojiPath = ''; foreach ($emojiLib as $value) { if ($word[1] == $value->alt) { //匹配上就转化为图片 $matchLibImg = true; $emojiImg = guid() . '.png'; $emojiPath = $emojiFullPath . $emojiImg; $baseImg = str_replace('data:image/png;base64,', '', $value->src); file_put_contents($emojiPath, base64_decode($baseImg)); break; } } if ($matchLibImg and $emojiPath) { //有表情图片 //得到表情 $emoji = imagecreatefrompng($emojiPath); //修改表情大小 $emoji = resizeImageSource($emoji, $tinyEmojiWidth, $tinyEmojiHeight); imagecopy($backgroundSource, $emoji, $xWidth, $yEmojiHeight, 0, 0, $tinyEmojiWidth, $tinyEmojiHeight);//复制表情 //删除临时表情 unlink($emojiPath); } $xWidth += $emojiWordSpace; } elseif ($wordType == 'text') { //普通文本直接写入 $text = $word[1]; $textLength = mb_strlen($text); $chineseWordsNum = utf8ChineseNum($text); //写入昵称 $text = mb_convert_encoding($text, "html-entities", "utf-8"); imagettftext($backgroundSource, $emojiSize, 0, $xWidth, $yTextHeight, $emojiColor, $emojiFont, $text); $xWidth += ($chineseWordsNum * $emojiWordSpace + ($textLength - $chineseWordsNum) * $wordSpace); } } } else { //写入昵称 $text = mb_convert_encoding($emoji, "html-entities", "utf-8"); imagettftext($backgroundSource, $emojiSize, 0, $xWidth, $yTextHeight, $emojiColor, $emojiFont, $text); } return $backgroundSource; } /**
* 重新定义图片的尺寸 * @param $img * @param $xMax * @param $yMax * @return false|resource */ function resizeImageSource($img, $xMax, $yMax) { $img2 = imagecreate($xMax, $yMax); imagecopyresized($img2, $img, 0, 0, 0, 0, $xMax, $yMax, imagesx($img), imagesy($img)); return $img2; } /** * 字符串中是否有emoji表情 * * @param $str * @return bool */ function haveEmoji($str) { preg_match_all('/./u', $str, $matches); foreach ($matches[0] as $word) { if (strlen($word) >= 4) { return true; } } return false; } /** * 分割字符串 并标记字符类型 * * @param $str * @return array */ function splitEmojiInStr($str) { $return = []; preg_match_all('/./u', $str, $matches); foreach ($matches[0] as $word) { if (strlen($word) >= 4) { $return[] = ['emoji', $word]; } else { $sum = count($return); if ($sum > 0) { if ($return[ count($return) - 1 ][0] == 'text') {//当前最后一位是普通字符 $return[ count($return) - 1 ][1] .= $word; } else { $return[] = ['text', $word]; } } else { $return[] = ['text', $word]; } } } return $return; } /** * 生成随机数 * * @return false|string */ function guid() { if (function_exists('com_create_guid')) return substr(strtolower(com_create_guid()), 1, -1); mt_srand(( double )microtime() * 10000); $charid = md5(uniqid(rand(), true)); $hyphen = chr(45);// "-" $uuid = substr($charid, 0, 8) . $hyphen . substr($charid, 8, 4) . $hyphen . substr($charid, 12, 4) . $hyphen . substr($charid, 16, 4) . $hyphen . substr($charid, 20, 12); return $uuid; } /** * utf8中文字数 * * @param $str * @return int */ function utf8ChineseNum($str) { preg_match_all('/./u', $str, $matches); $num = 0; foreach ($matches[0] as $v) { $num += strlen($v) == 3 ? 1 : 0; } return $num; }
有了这些方法后就可以过滤一遍后重新生成了:
// 普通中文格式转换一下 $text = mb_convert_encoding($text, "html-entities", "utf-8"); // 符号处理 $pictogram = toEntities($pictogram); // emoji $emojiWidth = 400; $emojiHeight = 56; $emojiSource = getEmojiImg($emoji, 32, $black, $normalFontPath, $emojiWidth, $emojiHeight, 99); // 普通中文 imagettftext($boardSource, 39, 0, 10, 100, $black, $normalFontPath, $text); // 符号 imagettftext($boardSource, 39, 0, 10, 190, $black, $normalFontPath, $pictogram); // emoji imagecopyresampled($boardSource, $emojiSource, 10, 230, 0, 0, $emojiWidth, $emojiHeight, $emojiWidth, $emojiHeight); imagedestroy($emojiSource);
重新生成后为:
其它处理
有时需将图片裁剪成圆形,如把头像变成圆形。
// 调用部分代码
$imgUrl = 'http://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKjMm8BLuicTicOTXCe2gLo0LoYbpxhWQtwSeIQ8NU1icbrnUBHHr9Iss8dYAuIHiaEZXSenPqRGgAcgQ/132'; $avatarString = getCurl($imgUrl); $avatarWidth = 120; $avatarHeight = 120; $newAvatarSource = makeCircularImage($avatarString, $avatarWidth, $avatarHeight); imagecopyresampled($boardSource, $newAvatarSource, 130, 130, 0, 0, $avatarWidth, $avatarHeight, $avatarWidth, $avatarHeight); /** * 制作圆形图片 * * @param $imgString * @param $width * @param $height * @return false|resource */ function makeCircularImage($imgString, $width, $height) { $originalSource = imagecreatefromstring($imgString); $realWH = getRealImage($originalSource, $width, $height); $width = $realWH['width']; $height = $realWH['height']; $newSource = imagecreatetruecolor($width, $height); imagesavealpha($newSource, true); $bg = imagecolorallocatealpha($newSource, 255, 255, 255, 127); imagefill($newSource, 0, 0, $bg); $r = $width / 2; //圆半径 for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgbColor = imagecolorat($originalSource, $x, $y); if (((($x - $r) * ($x - $r) + ($y - $r) * ($y - $r)) < ($r * $r))) { imagesetpixel($newSource, $x, $y, $rgbColor); } } } return $newSource; } /** * 得到真实图片尺寸 * * @param $imageResource * @param $width * @param $height * @return array */ function getRealImage($imageResource, $width, $height) { $realImageX = imagesx($imageResource); $realImageY = imagesy($imageResource); if ($realImageX < $width) { $width = $realImageX; } if ($realImageY < $height) { $height = $realImageY; } return ['width' => $width, 'height' => $height]; }
有时还可能碰到需要进行高斯模糊滤镜的处理:
function blurImage($imageSource, int $blurFactor = 3) { // blurFactor has to be an integer $blurFactor = round($blurFactor); $originalWidth = imagesx($imageSource); $originalHeight = imagesy($imageSource); $smallestWidth = ceil($originalWidth * pow(0.5, $blurFactor)); $smallestHeight = ceil($originalHeight * pow(0.5, $blurFactor)); // for the first run, the previous image is the original input $prevImage = $imageSource; $prevWidth = $originalWidth; $prevHeight = $originalHeight; // scale way down and gradually scale back up, blurring all the way for ($i = 0; $i < $blurFactor; $i += 1) { // determine dimensions of next image $nextWidth = $smallestWidth * pow(2, $i); $nextHeight = $smallestHeight * pow(2, $i); // resize previous image to next size $nextImage = imagecreatetruecolor($nextWidth, $nextHeight); imagecopyresized($nextImage, $prevImage, 0, 0, 0, 0, $nextWidth, $nextHeight, $prevWidth, $prevHeight); // apply blur filter imagefilter($nextImage, IMG_FILTER_GAUSSIAN_BLUR); // now the new image becomes the previous image for the next step $prevImage = $nextImage; $prevWidth = $nextWidth; $prevHeight = $nextHeight; } // scale back to original size and blur one more time imagecopyresized($imageSource, $nextImage, 0, 0, 0, 0, $originalWidth, $originalHeight, $nextWidth, $nextHeight); imagefilter($imageSource, IMG_FILTER_GAUSSIAN_BLUR); // clean up imagedestroy($prevImage); // return result return $imageSource; }