PHP GD 库实现微信小程序邀请海报

本文将基于 ThinkPHP 框架,使用 PHP GD 库实现一个完整的邀请海报生成功能,并重点解决头像颜色失真和透明通道丢失两个常见坑点。

目录

一、需求背景

二、环境准备

三、核心实现

3.1 创建真彩色画布

3.2 合成用户头像

3.3 绘制用户昵称(居中显示)

3.4 合成小程序码

3.5 保存海报并清理旧文件

四、完整代码

五、常见坑点总结

六、扩展建议


一、需求背景

在微信小程序项目中,经常需要为用户生成邀请海报,海报上包含:

  • 用户头像
  • 用户昵称
  • 小程序码(带邀请参数)

本文将基于 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 参数动态调整画布尺寸,适配不同分享场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JSON_L

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值