本文将基于 ThinkPHP 框架,使用 PHP GD 库实现一个完整的邀请海报生成功能,并重点解决头像颜色失真和透明通道丢失两个常见坑点。
目录
一、需求背景
在微信小程序项目中,经常需要为用户生成邀请海报,海报上包含:
- 用户头像
- 用户昵称
- 小程序码(带邀请参数)
本文将基于 ThinkPHP 框架,使用 PHP GD 库实现一个完整的邀请海报生成功能,并重点解决头像颜色失真和透明通道丢失两个常见坑点。
二、环境准备
- PHP 7.2+(需开启 GD 扩展)
- ThinkPHP 5.1 / 6.0
- 微信小程序 AppID / AppSecret
- 背景图 PNG 文件
- TTF 字体文件
检查 GD 扩展是否开启:
php -m | grep gd
三、核心实现
3.1 创建真彩色画布
很多开发者直接使用 imagecreatefrompng() 加载背景图后就开始绘制,但如果背景图是调色板模式(8-bit PNG),后续复制真彩色头像时颜色会被强制映射到有限的调色板,导致头像颜色严重失真。
解决方案:加载背景图后,立即转换为真彩色画布(24-bit)。
// 1. 加载原始背景图
$srcImage = imagecreatefrompng($bgPath);
$bgWidth = imagesx($srcImage);
$bgHeight = imagesy($srcImage);
// 2. 创建真彩色画布
$bgImage = imagecreatetruecolor($bgWidth, $bgHeight);
// 3. 保留透明通道(背景图有透明区域时必须设置)
imagealphablending($bgImage, false);
imagesavealpha($bgImage, true);
// 4. 将原图复制到真彩色画布
imagecopy($bgImage, $srcImage, 0, 0, 0, 0, $bgWidth, $bgHeight);
imagedestroy($srcImage);
// 5. 恢复混合模式,后续绘制头像和文字需要
imagealphablending($bgImage, true);
为什么需要 imagealphablending 和 imagesavealpha?
- imagealphablending($bgImage, false) — 关闭混合模式,确保复制时透明像素不会被覆盖为黑色
- imagesavealpha($bgImage, true) — 保存时保留 alpha 通道信息
- 绘制完背景后需要 imagealphablending($bgImage, true) 恢复混合模式,否则后续 imagecopyresampled 和 imagettftext 会异常
3.2 合成用户头像
使用 imagecreatefromstring() 读取头像(兼容 PNG/JPG/WebP),再用 imagecopyresampled() 缩放到目标尺寸并合成到画布。
// 读取头像(使用 imagecreatefromstring 避免 libpng ICCP 警告)
$avatarImage = imagecreatefromstring(file_get_contents($avatarPath));
$source_info = getimagesize($avatarPath);
// 头像居中显示,缩放到 160x160
$avatarSize = 160;
$avatarX = ($bgWidth - $avatarSize) / 2;
$avatarY = 600;
imagecopyresampled(
$bgImage, $avatarImage,
$avatarX, $avatarY, // 目标位置
0, 0, // 源起始位置
$avatarSize, $avatarSize, // 目标尺寸
$source_info[0], $source_info[1] // 源尺寸
);
注意:使用 imagecreatefromstring() 而非 imagecreatefrompng(),可以避免 libpng 的 ICCP 色彩配置文件警告。
3.3 绘制用户昵称(居中显示)
使用 imagettfbbox() 计算文字宽度,实现水平居中。
$nickname = $user['nickname'] ?: '用户';
$nicknameColor = imagecolorallocate($bgImage, 51, 51, 51); // 深灰色
$nicknameFontSize = 32;
$nicknameY = $avatarY + $avatarSize + 70;
// 计算文字宽度并居中
$nicknameBox = imagettfbbox($nicknameFontSize, 0, $fontPath, $nickname);
$nicknameWidth = $nicknameBox[2] - $nicknameBox[0];
$nicknameX = ($bgWidth - $nicknameWidth) / 2;
imagettftext($bgImage, $nicknameFontSize, 0, $nicknameX, $nicknameY, $nicknameColor, $fontPath, $nickname);
3.4 合成小程序码
调用微信 getUnlimitedQRCode 接口获取小程序码图片流,再合成到海报。
$config = get_addon_config('third');
$miniapp = new WechatMiniapp($config);
$scene = 'invitecode=' . $user['invite_code'];
$qrcodeResult = $miniapp->getUnlimitedQRCode($scene, 'pages/index/index', 280, 'trial', false);
// 将小程序码合成到背景图
$qrcodeImage = @imagecreatefromstring($qrcodeResult);
if ($qrcodeImage) {
$qrcodeW = imagesx($qrcodeImage);
$qrcodeH = imagesy($qrcodeImage);
$qrcodeX = ($bgWidth - $qrcodeW) / 2;
$qrcodeY = 1000;
imagecopy($bgImage, $qrcodeImage, $qrcodeX, $qrcodeY, 0, 0, $qrcodeW, $qrcodeH);
imagedestroy($qrcodeImage);
}
3.5 保存海报并清理旧文件
$filename = 'poster_' . $user['id'] . '_' . time() . '.png';
$filepath = ROOT_PATH . 'public/uploads/posterimage/' . $filename;
// 确保目录存在
$dir = dirname($filepath);
if (!file_exists($dir)) {
mkdir($dir, 0755, true);
}
// 保存 PNG 图片
imagepng($bgImage, $filepath);
imagedestroy($bgImage);
// 删除旧海报
if ($user['invite_url']) {
$this->deleteFile($user['invite_url']);
}
// 更新用户记录
$user['invite_url'] = '/uploads/posterimage/' . $filename;
$user->save();
四、完整代码
public function invitePoster()
{
try {
$user = $this->auth->getUser();
$qrcodeWidth = $this->request->param('width', 280);
// 获取背景图路径
$bgPath = ROOT_PATH . 'public/assets/img/invite_poster_bg.png';
if (!file_exists($bgPath)) {
$this->error('背景图不存在');
}
// 获取字体文件路径
$fontPath = ROOT_PATH . 'public/assets/fonts/SourceHanSansK-Regular.ttf';
if (!file_exists($fontPath)) {
$this->error('字体文件不存在');
}
// 创建真彩色画布(避免头像颜色失真)
$srcImage = imagecreatefrompng($bgPath);
if (!$srcImage) {
$this->error('创建画布失败');
}
$bgWidth = imagesx($srcImage);
$bgHeight = imagesy($srcImage);
$bgImage = imagecreatetruecolor($bgWidth, $bgHeight);
imagealphablending($bgImage, false);
imagesavealpha($bgImage, true);
imagecopy($bgImage, $srcImage, 0, 0, 0, 0, $bgWidth, $bgHeight);
imagedestroy($srcImage);
imagealphablending($bgImage, true);
// 合成用户头像
$avatarUrl = $user['avatar'] ?: '';
if (!$avatarUrl) {
$this->error('用户头像不存在');
}
$avatarPath = ROOT_PATH . 'public' . $avatarUrl;
$avatarImage = imagecreatefromstring(file_get_contents($avatarPath));
$source_info = getimagesize($avatarPath);
$avatarSize = 160;
$avatarX = ($bgWidth - $avatarSize) / 2;
$avatarY = 600;
imagecopyresampled($bgImage, $avatarImage, $avatarX, $avatarY, 0, 0, $avatarSize, $avatarSize, $source_info[0], $source_info[1]);
// 绘制用户昵称(居中)
$nickname = $user['nickname'] ?: '用户';
$nicknameColor = imagecolorallocate($bgImage, 51, 51, 51);
$nicknameFontSize = 32;
$nicknameY = $avatarY + $avatarSize + 70;
$nicknameBox = imagettfbbox($nicknameFontSize, 0, $fontPath, $nickname);
$nicknameWidth = $nicknameBox[2] - $nicknameBox[0];
$nicknameX = ($bgWidth - $nicknameWidth) / 2;
imagettftext($bgImage, $nicknameFontSize, 0, $nicknameX, $nicknameY, $nicknameColor, $fontPath, $nickname);
// 获取并合成小程序码
$config = get_addon_config('third');
$miniapp = new WechatMiniapp($config);
$scene = 'invitecode=' . $user['invite_code'];
$qrcodeResult = $miniapp->getUnlimitedQRCode($scene, 'pages/index/index', $qrcodeWidth, 'trial', false);
if (is_array($qrcodeResult) && isset($qrcodeResult['error'])) {
imagedestroy($bgImage);
$this->error('获取小程序码失败: ' . $qrcodeResult['error']);
}
$qrcodeImage = @imagecreatefromstring($qrcodeResult);
if ($qrcodeImage) {
$qrcodeW = imagesx($qrcodeImage);
$qrcodeH = imagesy($qrcodeImage);
$qrcodeX = ($bgWidth - $qrcodeW) / 2;
$qrcodeY = 1000;
imagecopy($bgImage, $qrcodeImage, $qrcodeX, $qrcodeY, 0, 0, $qrcodeW, $qrcodeH);
imagedestroy($qrcodeImage);
}
// 保存海报
$filename = 'poster_' . $user['id'] . '_' . time() . '.png';
$filepath = ROOT_PATH . 'public/uploads/posterimage/' . $filename;
$dir = dirname($filepath);
if (!file_exists($dir)) {
mkdir($dir, 0755, true);
}
imagepng($bgImage, $filepath);
imagedestroy($bgImage);
// 删除旧海报并更新记录
if ($user['invite_url']) {
$this->deleteFile($user['invite_url']);
}
$user['invite_url'] = '/uploads/posterimage/' . $filename;
$user->save();
$this->success('success', [
'invite_url' => local_url('/uploads/posterimage/' . $filename),
]);
} catch (Exception $e) {
$this->error('生成海报失败: ' . $e->getMessage());
}
}
五、常见坑点总结
| 问题 | 原因 | 解决方案 |
| 头像颜色失真/偏色 | 背景图是 8-bit 调色板 PNG | 用 imagecreatetruecolor() 转为 24-bit 真彩色画布 |
| 透明区域变黑色 | 未设置 alpha 通道 | imagealphablending(false) + imagesavealpha(true) |
| libpng ICCP 警告 | imagecreatefrompng() 读取含 ICC 配置文件的 PNG | 改用 imagecreatefromstring() |
| 文字不居中 | 未计算文字实际宽度 | 用 imagettfbbox() 获取文字边界框后计算居中坐标 |
| 旧海报堆积 | 每次生成新海报未清理旧文件 | 保存前删除用户旧海报文件 |
六、扩展建议
1. 圆形头像裁剪:当前头像是正方形,如需圆形效果,可在合成前用 GD 创建圆形遮罩
2. 缓存机制:海报生成较耗时,可对同一用户做缓存,避免重复生成
3. 异步生成:用户量大时建议用队列异步生成,前端 polling 获取结果
4. 多尺寸适配:根据 width 参数动态调整画布尺寸,适配不同分享场景

7488

被折叠的 条评论
为什么被折叠?



