如何使用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;
}