quanwei
2 days ago 73b874c72ad55eb9eef21c36160ac0de58f0189e
admin/app/common/service/business/Poster.php
@@ -20,6 +20,7 @@
    /* @var array $config 名片设置 */
    private $config;
    public $template;
    private $businessName;
    /**
     * 构造方法
@@ -27,11 +28,12 @@
     * @param $dealer
     * @throws \Exception
     */
    public function __construct($dealer)
    public function __construct($dealer,$businessName='business')
    {
        parent::__construct();
        // 用户信息
        $this->dealer = $dealer;
        $this->businessName = $businessName;
        $this->template = (new Template())->detail($dealer['template_id']);
        // 名片设置
        $this->config = json_decode($this->template['style'], true);
@@ -46,8 +48,8 @@
     */
    public function getImage($isType = true)
    {
        if (file_exists($this->getPosterPath('business')) && $isType) {
            return $this->getPosterUrl('business');
        if (file_exists($this->getPosterPath($this->businessName)) && $isType) {
            return $this->getPosterUrl($this->businessName);
        }
        // 小程序id
        $appId = $this->dealer['app_id'];
@@ -61,19 +63,19 @@
            $avatarUrl = $this->saveTempImage($appId, $this->dealer['user']['avatarUrl'], 'avatar');
        }
        $logo = '';
        if ($this->dealer['logoImage']['file_path']) {
        if (!empty($this->dealer['logoImage']['file_path'])) {
            // 2. 下载logo
            $logo = $this->saveTempImage($appId, $this->dealer['logoImage']['file_path'], 'logo');
        }
        // 4. 拼接名片
        return $this->savePoster($backdrop, $avatarUrl, 'business', $logo);
        return $this->savePoster($backdrop, $avatarUrl, $this->businessName, $logo);
    }
    /**
     * 名片图文件路径
     * @return string
     */
    private function getPosterPath($name)
    public function getPosterPath($name)
    {
        // 保存路径
        $tempPath = $_SERVER['DOCUMENT_ROOT']. '/temp/'.$this->template['app_id'] . '/'.$name.'/';
@@ -111,21 +113,63 @@
    private function savePoster($backdrop, $avatarUrl, $imageUrl, $logo)
    {
        // 创建画布
        list($width, $height) = getimagesize($backdrop);
        // 获取背景图信息,判断图片类型
        $backdropInfo = getimagesize($backdrop);
        $width = $backdropInfo[0];
        $height = $backdropInfo[1];
        // 根据原图类型创建对应类型的画布,提高质量
        $imageType = $backdropInfo[2];
        // 创建真彩色画布,确保足够的色彩深度
        $newImage = imagecreatetruecolor($width, $height);
        $backdropIm = $this->imagEcr($backdrop);
        // 启用抗锯齿
        imageantialias($newImage, true);
        // 将第一张图片覆盖在新图像上
        imagecopy($newImage, $backdropIm, 0, 0, 0, 0, $width, $height);
        // 启用alpha混合模式,确保透明度正常
        imagealphablending($newImage, true);
        // 保存透明度
        imagesavealpha($newImage, true);
        // 加载背景图
        $backdropIm = $this->imagEcr($backdrop);
        // 将背景图复制到画布(使用更好的插值方法)
        imagecopyresampled($newImage, $backdropIm, 0, 0, 0, 0, $width, $height, $width, $height);
        if (!empty($this->config['icon'])) {
            foreach ($this->config['icon'] as $key => $value) {
                $this->addImagecopy($newImage, $value);
            }
        }
        // 脱敏处理
        if($this->businessName=='desensitization'){
            // 手机号脱敏:保留前3位和后4位
            if(!empty($this->dealer['mobile'])){
                $this->dealer['mobile'] = $this->maskPhoneNumber($this->dealer['mobile']);
            }
            if(!empty($this->dealer['mobile_phone'])){
                $this->dealer['mobile_phone'] = $this->maskPhoneNumber($this->dealer['mobile_phone']);
            }
            // 微信脱敏:保留前2位和后2位
            if(!empty($this->dealer['wechat'])){
                $this->dealer['wechat'] = $this->maskWechat($this->dealer['wechat']);
            }
            // 邮箱脱敏:保留域名和前2位用户名
            if(!empty($this->dealer['mailbox'])){
                $this->dealer['mailbox'] = $this->maskEmail($this->dealer['mailbox']);
            }
            // 电话脱敏:保留前3位和后4位
            if(!empty($this->dealer['phone'])){
                $this->dealer['phone'] = $this->maskPhoneNumber($this->dealer['phone']);
            }
        }
        // 写入地址
        //$this->addText($newImage, 'address', '地址:', 0, false, $width);
        $this->addText($newImage, 'address', '地址:', 0, false, $width);
        if ($this->config['avatar']['display'] == 1) {
            // 生成圆形用户头像
            $this->config['avatar']['style'] === 'circle' && $this->circular($avatarUrl, $avatarUrl);
@@ -138,6 +182,7 @@
            $avatarY = $this->config['avatar']['top'];
            // 打开用户头像
            $avatarIm = $this->imagEcr($avatarUrl);
            // 使用更好的插值方法复制头像(使用imagecopy保持原有质量)
            imagecopy($newImage, $avatarIm, $avatarX, $avatarY, 0, 0, $avatarWidth, $avatarWidth);
        }
        if (is_file($logo) && $this->config['logo']['display'] == 1) {
@@ -153,46 +198,109 @@
            $logoY = $this->config['logo']['top'];
            // 打开用户logo
            $logoImage = $this->imagEcr($logo);
            // 使用更好的插值方法复制logo(使用imagecopy保持原有质量)
            imagecopy($newImage, $logoImage, $logoX, $logoY, 0, 0, $logoWidth, $logoHeight);
        }
        // 写入用户昵称
        $this->addText($newImage, 'name');
        $this->addText($newImage, 'name', '', 0, false, $width);
        // 写入公司列表
        $unitBaseTop = 0;
        $unitFontSize = $this->config['unit'][0]['fontSize'];
        foreach ($this->dealer['unit'] as $key => $value) {
            // 写入公司
            $this->addText($newImage, 'unit', '', $key, true);
            if($key > 0){
                $this->config['unit'][$key] = $this->config['unit'][0];
                // 计算偏移量:每个公司增加基于前一个公司实际行数的距离
                $this->config['unit'][$key]['top'] = $this->config['unit'][0]['top'] + $unitBaseTop;
            }
            // 写入公司,获取实际占用的行数
            $lineCount = $this->addText($newImage, 'unit', '', $key, true, $width);
            // 计算下一个公司的偏移量:(行数 - 1) * 行高 + 行间距
            $unitLineHeight = $this->getLineHeight($unitFontSize);
            $unitLineSpacing = $this->getLineSpacing($unitFontSize);
            if ($key > 0) {
                $unitBaseTop += ($lineCount * $unitLineHeight) + $unitLineSpacing;
            } else {
                $unitBaseTop = ($lineCount * $unitLineHeight) + $unitLineSpacing;
            }
        }
        foreach ($this->dealer['address'] as $key => $value) {
            // 写入地址
            $this->addText($newImage, 'address', '', $key, true);
        // 写入职位列表
        if (!empty($this->dealer['duties']) && !empty($this->config['duties'])) {
            $dutiesBaseTop = 0;
            $dutiesFontSize = $this->config['duties'][0]['fontSize'];
            foreach ($this->dealer['duties'] as $key => $value) {
                if($key > 0){
                    $this->config['duties'][$key] = $this->config['duties'][0];
                    // 计算偏移量:每个职位增加基于前一个职位实际行数的距离
                    $this->config['duties'][$key]['top'] = $this->config['duties'][0]['top'] + $dutiesBaseTop;
                }
                // 写入职位,获取实际占用的行数
                $lineCount = $this->addText($newImage, 'duties','', $key, true, $width);
                // 计算下一个职位的偏移量:(行数 - 1) * 行高 + 行间距
                $dutiesLineHeight = $this->getLineHeight($dutiesFontSize);
                $dutiesLineSpacing = $this->getLineSpacing($dutiesFontSize);
                if ($key > 0) {
                    $dutiesBaseTop += ($lineCount * $dutiesLineHeight) + $dutiesLineSpacing;
                } else {
                    $dutiesBaseTop = ($lineCount * $dutiesLineHeight) + $dutiesLineSpacing;
                }
            }
        }
        foreach ($this->dealer['duties'] as $key => $value) {
            // 写入职务
            $this->addText($newImage, 'duties', '', $key, true);
        }
        // 写入职位
        //$this->addText($newImage, 'duties');
        // 写入position列表(备用字段)
        if (!empty($this->dealer['position']) && !empty($this->config['position'])) {
            $positionBaseTop = 0;
            $positionFontSize = $this->config['position'][0]['fontSize'];
            foreach ($this->dealer['position'] as $key => $value) {
                // 写入公司
                $this->addText($newImage, 'position', '', $key, true);
                if($key > 0){
                    $this->config['position'][$key] = $this->config['position'][0];
                    // 计算偏移量:每个position增加基于前一个实际行数的距离
                    $this->config['position'][$key]['top'] = $this->config['position'][0]['top'] + $positionBaseTop;
                }
                // 写入position,获取实际占用的行数
                $lineCount = $this->addText($newImage, 'position', '', $key, true, $width);
                // 计算下一个position的偏移量:(行数 - 1) * 行高 + 行间距
                $positionLineHeight = $this->getLineHeight($positionFontSize);
                $positionLineSpacing = $this->getLineSpacing($positionFontSize);
                if ($key > 0) {
                    $positionBaseTop += ($lineCount * $positionLineHeight) + $positionLineSpacing;
                } else {
                    $positionBaseTop = ($lineCount * $positionLineHeight) + $positionLineSpacing;
                }
            }
        }
        // 写入手机号
        $this->addText($newImage, 'mobile', '手机:');
        // 写入微信
        $this->addText($newImage, 'mobile', '手机:', 0, false, $width);
        if (!empty($this->dealer['wechat']) && !empty($this->config['wechat'])) {
            // 写入微信
            $this->addText($newImage, 'wechat', '微信:', 0, false, $width);
        }
        // 写入邮箱
        if ($this->dealer['mailbox']) {
            // 写入邮箱
            $this->addText($newImage, 'mailbox', '邮箱:');
            $this->addText($newImage, 'mailbox', '邮箱:', 0, false, $width);
        }
        if ($this->dealer['phone']) {
            $this->addText($newImage, 'phone', '电话:');
            $this->addText($newImage, 'phone', '电话:', 0, false, $width);
        }
        // 保存图片
        imagejpeg($newImage, $this->getPosterPath($imageUrl),100); // 根据需要选择合适的函数(如imagepng、imagegif等)
        // 根据背景图类型选择保存格式
        $imageType = $backdropInfo[2];
        $savePath = $this->getPosterPath($imageUrl);
        if ($imageType == IMAGETYPE_PNG) {
            // PNG格式,使用最高质量(9)
            imagepng($newImage, $savePath, 9);
        } elseif ($imageType == IMAGETYPE_GIF) {
            // GIF格式
            imagegif($newImage, $savePath);
        } else {
            // JPEG格式,使用最高质量(100)
            imagejpeg($newImage, $savePath, 100);
        }
        // 清理内存
        imagedestroy($newImage);
        imagedestroy($backdropIm);
        return $this->getPosterUrl($imageUrl);
    }
@@ -227,10 +335,17 @@
        imagesavealpha($targetImage, true);
        $bg = imagecolorallocatealpha($targetImage, 255, 255, 255, 127);
        imagefill($targetImage, 0, 0, $bg);
        imagecopyresampled($targetImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
        imagepng($targetImage, $imageUrl,0); // 根据需要选择合适的函数(如imagepng、imagegif等)
        imagecopyresampled($targetImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height); // 保存时使用最高质量
        $ext = pathinfo($imageUrl, PATHINFO_EXTENSION);
        if ($ext == 'png'){
            imagepng($targetImage, $imageUrl, 9); // PNG使用最高质量(9)
        }else{
            imagejpeg($targetImage, $imageUrl, 100); // JPEG使用最高质量
        }
        // 清理内存
        imagedestroy($targetImage);
        imagedestroy($image);
    }
    /**
@@ -256,49 +371,60 @@
     * @param $key
     * @param $type
     * @param $width
     * @return array|bool
     * @return array|bool|int 返回实际占用的行数(仅对type=true时有效)
     */
    public function addText($editor, $name, $text = '', $key = 0, $type = false, $width = 0)
    {
        $fontPath = $_SERVER['DOCUMENT_ROOT']. '/fonts/MSYH.TTC';
        if ($type) {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
            $colorResource=self::colorResource($this->config[$name][$key]['color'],$editor);
            return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $this->dealer[$name][$key]);
            // 确保配置数组存在该索引,不存在则使用索引0
            $configKey = isset($this->config[$name][$key]) ? $key : 0;
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name][$configKey]['fontSize'], $this->config[$name][$configKey]['left'],$this->config[$name][$configKey]['top']);
            $colorResource=self::colorResource($this->config[$name][$configKey]['color'],$editor);
            // 智能计算最大宽度
            $maxWidth = $this->calculateFieldMaxWidth($this->config[$name][$configKey]['left'], $this->config[$name][$configKey]['top'], $width);
            // 写入支持换行的文本
            return $this->writeTextWithWrap($editor, $text . $this->dealer[$name][$key], $fontX, $fontY, $fontSize, $colorResource, $fontPath, $maxWidth);
        } else if ($name == 'mobile') {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name]['fontSize'], $this->config[$name]['left'],$this->config[$name]['top']);
            $text = $text . $this->dealer['mobile'];
            if (!empty($this->dealer['mobile_phone'])) {
                $text = $text . ' / ' . $this->dealer['mobile_phone'];
            }
            $colorResource=self::colorResource($this->config[$name]['color'],$editor);
            return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text);
        }  /*else if ($name == 'duties') {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
            // 智能计算最大宽度
            $maxWidth = $this->calculateFieldMaxWidth($this->config[$name]['left'], $this->config[$name]['top'], $width);
            // 写入支持换行的文本
            $this->writeTextWithWrap($editor, $text, $fontX, $fontY, $fontSize, $colorResource, $fontPath, $maxWidth);
        } else if ($name == 'duties') {
            // 确保配置数组存在该索引,不存在则使用索引0
            $configKey = isset($this->config[$name][$key]) ? $key : 0;
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name][$configKey]['fontSize'], $this->config[$name][$configKey]['left'],$this->config[$name][$configKey]['top']);
            $duties = $this->dealer['duties'][$key];
           if (!empty($this->dealer['duties'][1])) {
                $duties = $duties . ' / ' . $this->dealer['duties'][1];
            }
            if (!empty($this->dealer['duties'][2])) {
                $duties = $duties . ' / ' . $this->dealer['duties'][2];
            }
            $colorResource=self::colorResource($this->config['duties'][$key]['color'],$editor);
            return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $duties);
        }*/ else if ($name == 'address') {
            $colorResource=self::colorResource($this->config['duties'][0]['color'],$editor);
            // 智能计算最大宽度
            $maxWidth = $this->calculateFieldMaxWidth($this->config[$name][$configKey]['left'], $this->config[$name][$configKey]['top'], $width);
            // 写入支持换行的文本
            return $this->writeTextWithWrap($editor, $text . $duties, $fontX, $fontY, $fontSize, $colorResource, $fontPath, $maxWidth);
        } else if ($name == 'address') {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name][$key]['fontSize'], $this->config[$name][$key]['left'],$this->config[$name][$key]['top']);
            $title = $this->dealer['address'][$key];
            $title = $this->dealer['address'][0];
            if(!empty($this->dealer['region'])){
                $title = $this->dealer['region']['province'] . $this->dealer['region']['city']. $this->dealer['region']['region'] . $title;
            }
            $strlen = mb_strlen($title, 'utf-8');
            $left = $width - $this->config['address'][$key]['left'] - $fontSize;
            $titleNum = bcdiv($left, $this->config['address'][$key]['fontSize']);
            $the_box = $this->config['address'][$key]['fontSize'] * 3;
            while ($width < ($titleNum * $this->config['address'][$key]['fontSize'] + $the_box + $fontX)) {
            $left = $width - $this->config['address'][0]['left'] - $fontSize;
            $titleNum = bcdiv($left, $this->config['address'][0]['fontSize']);
            $the_box = $this->config['address'][0]['fontSize'] * 3;
            while ($width < ($titleNum * $this->config['address'][0]['fontSize'] + $the_box + $fontX)) {
                $titleNum--;
            };
            if ($strlen > $titleNum && $titleNum) {
            if ($strlen > $titleNum && $titleNum>0) {
                $strArr = self::mbStrSplit($title, $titleNum);
            }
            $colorResource=self::colorResource($this->config['address'][$key]['color'],$editor);
            if ($strlen > $titleNum && $titleNum) {
            $colorResource=self::colorResource($this->config['address'][0]['color'],$editor);
            if ($strlen > $titleNum && $titleNum>0) {
                $y = $fontY + 10;
                foreach ($strArr as $k => $v) {
                    if ($k == 0) {
@@ -316,9 +442,11 @@
        } else {
            list($fontSize, $fontX,$fontY)  = self::SizeLeftTop($this->config[$name]['fontSize'], $this->config[$name]['left'],$this->config[$name]['top']);
            $colorResource=self::colorResource($this->config[$name]['color'],$editor);
            return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $this->dealer[$name]); // path/to/font.ttf为自定义字体路径
            // 智能计算最大宽度
            $maxWidth = $this->calculateFieldMaxWidth($this->config[$name]['left'], $this->config[$name]['top'], $width);
            // 写入支持换行的文本
            $this->writeTextWithWrap($editor, $text . $this->dealer[$name], $fontX, $fontY, $fontSize, $colorResource, $fontPath, $maxWidth);
        }
    }
    /**
@@ -396,6 +524,82 @@
    }
    /**
     * 手机号/座机号脱敏处理
     * @param $phone
     * @return string
     */
    private function maskPhoneNumber($phone){
        $len = strlen($phone);
        if($len <= 7) return $phone; // 太短的号码不脱敏
        // 检查是否是带区号的座机号(包含-或空格)
        if(strpos($phone, '-') !== false || strpos($phone, ' ') !== false){
            // 座机号脱敏:保留区号和最后4位
            $parts = preg_split('/[-\s]/', $phone);
            // 确保有至少两部分(区号和号码)
            if(count($parts) >= 2){
                $areaCode = $parts[0]; // 区号
                $number = end($parts); // 号码部分
                $separator = strpos($phone, '-') !== false ? '-' : ' '; // 保持原始分隔符
                $numLen = strlen($number);
                if($numLen <= 4) return $phone; // 号码太短不脱敏
                $numSuffix = substr($number, -4); // 保留号码后4位
                $stars = str_repeat('*', $numLen - 4);
                // 重新组合:区号 + 分隔符 + 掩码 + 后4位
                return $areaCode . $separator . $stars . $numSuffix;
            }
        }
        // 普通手机号脱敏:保留前3位和后4位
        $prefix = substr($phone, 0, 3);
        $suffix = substr($phone, -4);
        $stars = str_repeat('*', $len - 7);
        return $prefix . $stars . $suffix;
    }
    /**
     * 微信脱敏处理
     * @param $wechat
     * @return string
     */
    private function maskWechat($wechat){
        $len = strlen($wechat);
        if($len <= 4) return $wechat; // 太短的微信号不脱敏
        $prefix = substr($wechat, 0, 2);
        $suffix = substr($wechat, -2);
        $stars = str_repeat('*', $len - 4);
        return $prefix . $stars . $suffix;
    }
    /**
     * 邮箱脱敏处理
     * @param $email
     * @return string
     */
    private function maskEmail($email){
        $parts = explode('@', $email);
        if(count($parts) != 2) return $email;
        $username = $parts[0];
        $domain = $parts[1];
        $len = strlen($username);
        if($len <= 2) return $email; // 太短的用户名不脱敏
        $prefix = substr($username, 0, 2);
        $stars = str_repeat('*', $len - 2);
        return $prefix . $stars . '@' . $domain;
    }
    /**
     * 获取颜色
     * @param $color
     * @param $editor
@@ -419,13 +623,200 @@
     * @param $fontSize
     * @param $left
     * @param $top
     * @return array
     * @return array [fontSizePt, left, top]
     */
    private function SizeLeftTop($fontSize,$left,$top){
        $data[0] = $fontSize * 0.76;
        $data[1]  = $left;
        $data[2]  = $top + $fontSize;
        // px到pt转换系数:1px ≈ 0.75pt (GD库使用磅pt作为字体单位)
        // 使用更精确的转换系数
        $fontSizePt = $fontSize * 0.75;
        // 调整top位置,使文字基线对齐
        // imagettftext的y坐标是文字基线位置,不是文字顶部
        // 需要加上字体大小来使文字显示在期望的位置
        $data[0] = $fontSizePt;  // 字体大小(pt)
        $data[1]  = (float)$left;  // 左边距(px)
        $data[2]  = (float)$top + $fontSize;  // 顶部位置(px),加字体大小以校正基线
        return $data;
    }
    /**
     * 获取文本实际高度
     * @param $fontSizePx 字体大小(px)
     * @return float 行高(px)
     */
    private function getLineHeight($fontSizePx) {
        // 行高 = 字体大小 * 1.5 (更舒适的行间距)
        return $fontSizePx * 1.5;
    }
    /**
     * 获取行间距
     * @param $fontSizePx 字体大小(px)
     * @return float 行间距(px)
     */
    private function getLineSpacing($fontSizePx) {
        // 行间距 = 字体大小 * 0.5 (50%的字体大小作为间距)
        return $fontSizePx * 0.5;
    }
    /**
     * 智能计算字段的最大宽度
     * 根据右边是否有其他字段来动态计算maxWidth
     * @param $left
     * @param $top
     * @param $backdropWidth
     * @return float
     */
    private function calculateFieldMaxWidth($left, $top, $backdropWidth = 375)
    {
        $rightFields = [];
        $tolerance = 20; // 容差:判断是否在同一行的像素范围
        // 收集所有可能的字段
        $fields = [];
        // 公司
        if (!empty($this->config['unit'][0])) {
            $fields[] = [
                'name' => 'unit',
                'left' => $this->config['unit'][0]['left'],
                'top' => $this->config['unit'][0]['top']
            ];
        }
        // 职位
        if (!empty($this->config['duties'][0])) {
            $fields[] = [
                'name' => 'duties',
                'left' => $this->config['duties'][0]['left'],
                'top' => $this->config['duties'][0]['top']
            ];
        }
        // 姓名
        if (!empty($this->config['name'])) {
            $fields[] = [
                'name' => 'name',
                'left' => $this->config['name']['left'],
                'top' => $this->config['name']['top']
            ];
        }
        // 地址
        if (!empty($this->config['address'][0])) {
            $fields[] = [
                'name' => 'address',
                'left' => $this->config['address'][0]['left'],
                'top' => $this->config['address'][0]['top']
            ];
        }
        // 邮箱
        if (!empty($this->config['mailbox'])) {
            $fields[] = [
                'name' => 'mailbox',
                'left' => $this->config['mailbox']['left'],
                'top' => $this->config['mailbox']['top']
            ];
        }
        // 手机
        if (!empty($this->config['mobile'])) {
            $fields[] = [
                'name' => 'mobile',
                'left' => $this->config['mobile']['left'],
                'top' => $this->config['mobile']['top']
            ];
        }
        // 微信
        if (!empty($this->config['wechat'])) {
            $fields[] = [
                'name' => 'wechat',
                'left' => $this->config['wechat']['left'],
                'top' => $this->config['wechat']['top']
            ];
        }
        // 电话
        if (!empty($this->config['phone'])) {
            $fields[] = [
                'name' => 'phone',
                'left' => $this->config['phone']['left'],
                'top' => $this->config['phone']['top']
            ];
        }
        // 找出右边最近的字段
        // 判断标准: left > 当前left, 且top在合理范围内(±20px)
        foreach ($fields as $field) {
            if ($field['left'] > $left && abs($field['top'] - $top) <= $tolerance) {
                $rightFields[] = $field;
            }
        }
        // 如果右边有字段,取最近的一个的左边距
        if (count($rightFields) > 0) {
            // 按left排序
            usort($rightFields, function($a, $b) {
                return $a['left'] - $b['left'];
            });
            return max($rightFields[0]['left'] - $left - 10, 50);
        }
        // 如果右边没有字段,使用名片边缘
        return max($backdropWidth - $left - 10, 100);
    }
    /**
     * 写入支持换行的文本
     * @param $editor
     * @param $text
     * @param $x
     * @param $y
     * @param $fontSize
     * @param $color
     * @param $fontPath
     * @param $maxWidth
     * @return int 返回实际使用的行数
     */
    private function writeTextWithWrap($editor, $text, $x, $y, $fontSize, $color, $fontPath, $maxWidth)
    {
        $fontSizePt = $fontSize * 0.75; // px转pt
        $lineHeight = $this->getLineHeight($fontSize); // 获取精确的行高
        $lines = [];
        $currentLine = '';
        // 按字符分割文本
        $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
        foreach ($chars as $char) {
            $testLine = $currentLine . $char;
            $bbox = imagettfbbox($fontSizePt, 0, $fontPath, $testLine);
            $lineWidth = $bbox[2] - $bbox[0];
            // 使用maxWidth减去一点边距,确保不超出边界
            if ($lineWidth > $maxWidth - 2 && $currentLine !== '') {
                $lines[] = $currentLine;
                $currentLine = $char;
            } else {
                $currentLine = $testLine;
            }
        }
        if ($currentLine !== '') {
            $lines[] = $currentLine;
        }
        // 写入每一行
        foreach ($lines as $index => $line) {
            // 第一行使用原始y坐标,后续行加上行高偏移
            $lineY = $y + ($index * $lineHeight);
            imagettftext($editor, $fontSizePt, 0, $x, $lineY, $color, $fontPath, $line);
        }
        // 返回实际占用的行数
        return count($lines);
    }
}