dealer = $dealer; $this->businessName = $businessName; $this->template = (new Template())->detail($dealer['template_id']); // 名片设置 $this->config = json_decode($this->template['style'], true); } /** * 获取用户名片 * @return string * @throws \app\common\exception\BaseException * @throws \think\exception\DbException * @throws \Exception */ public function getImage($isType = true) { if (file_exists($this->getPosterPath($this->businessName)) && $isType) { return $this->getPosterUrl($this->businessName); } // 小程序id $appId = $this->dealer['app_id']; // 1. 下载背景图 $backdrop = $this->saveTempImage($appId, $this->config['backdrop']['src'], 'backdrop'); if ($this->dealer['file_path']) { // 2. 下载用户头像 $avatarUrl = $this->saveTempImage($appId, $this->dealer['file_path'], 'avatar'); } else { // 2. 下载用户头像 $avatarUrl = $this->saveTempImage($appId, $this->dealer['user']['avatarUrl'], 'avatar'); } $logo = ''; 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, $this->businessName, $logo); } /** * 名片图文件路径 * @return string */ public function getPosterPath($name) { // 保存路径 $tempPath = $_SERVER['DOCUMENT_ROOT']. '/temp/'.$this->template['app_id'] . '/'.$name.'/'; !is_dir($tempPath) && mkdir($tempPath, 0755, true); return $tempPath . $this->getPosterName(); } /** * 名片文件名称 * @return string */ private function getPosterName() { return md5('poster_' . $this->dealer['business_card_id']) . '.png'; } /** * 名片url * @return string */ private function getPosterUrl($name) { return base_url() . '/temp/' . $this->template['app_id'] . '/' .$name . '/' . $this->getPosterName() . '?t=' . time(); } /** * 拼接名片 * @param $backdrop * @param $avatarUrl * @param $imageUrl * @param $logo * @return string * @throws \Exception */ private function savePoster($backdrop, $avatarUrl, $imageUrl, $logo) { // 获取背景图信息,判断图片类型 $backdropInfo = getimagesize($backdrop); $width = $backdropInfo[0]; $height = $backdropInfo[1]; // 根据原图类型创建对应类型的画布,提高质量 $imageType = $backdropInfo[2]; // 创建真彩色画布,确保足够的色彩深度 $newImage = imagecreatetruecolor($width, $height); // 启用抗锯齿 imageantialias($newImage, true); // 启用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); if ($this->config['avatar']['display'] == 1) { // 生成圆形用户头像 $this->config['avatar']['style'] === 'circle' && $this->circular($avatarUrl, $avatarUrl); // 重设用户头像宽高 $avatarWidth = $this->config['avatar']['width']; //重设图片大小 $this->resizeExact($avatarUrl,$avatarWidth,$avatarWidth); // 用户头像添加到背景图 $avatarX = $this->config['avatar']['left']; $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) { // 生成圆形logo $this->config['logo']['style'] === 'circle' && $this->circular($logo, $logo); //调整后大小 $logoWidth = round($this->config['logo']['width']); $logoHeight = round($this->config['logo']['height']); //重设图片大小 $this->resizeExact($logo,$logoWidth,$logoHeight); // 用户logo添加到背景图 $logoX = $this->config['logo']['left']; $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', '', 0, false, $width); // 写入公司列表 $unitBaseTop = 0; $unitFontSize = $this->config['unit'][0]['fontSize']; foreach ($this->dealer['unit'] as $key => $value) { 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; } } // 写入职位列表 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; } } } // 写入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) { 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', '手机:', 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', '邮箱:', 0, false, $width); } if ($this->dealer['phone']) { $this->addText($newImage, 'phone', '电话:', 0, false, $width); } // 保存图片 // 根据背景图类型选择保存格式 $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); } /** * 打开图片 * @param $image * @return false|resource */ public function imagEcr($image) { // 创建画布 return imagecreatefromstring(file_get_contents($image)); } /** * 重设图片大小 * @param $image * @param $newWidth * @param $newHeight * @return void */ public function resizeExact($imageUrl, $newWidth, $newHeight ) { $image=imagecreatefromstring(file_get_contents($imageUrl)); // 调整图片大小 $targetImage = imagecreatetruecolor($newWidth, $newHeight); imageantialias($targetImage, true); //获取图片大小 $width = imagesx($image); $height = imagesy($image); // 这一句一定要有 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); // 保存时使用最高质量 $ext = pathinfo($imageUrl, PATHINFO_EXTENSION); if ($ext == 'png'){ imagepng($targetImage, $imageUrl, 9); // PNG使用最高质量(9) }else{ imagejpeg($targetImage, $imageUrl, 100); // JPEG使用最高质量 } // 清理内存 imagedestroy($targetImage); imagedestroy($image); } /** * 添加图标 * @param $newImage * @param $data * @return void */ public function addImagecopy($newImage,$data){ $src=$this->saveTempImage($this->template['app_id'], $data['src'], 'icon'); $this->resizeExact($src,$data['width'], $data['height']); $backdropIm = $this->imagEcr($src); // 将第一张图片覆盖在新图像上 imagecopy($newImage, $backdropIm, $data['left'], $data['top'], 0, 0, $data['width'], $data['height']); } /** * 写入文字 * @param $editor * @param $backdropImage * @param $name * @param $text * @param $key * @param $type * @param $width * @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) { // 确保配置数组存在该索引,不存在则使用索引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); // 智能计算最大宽度 $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]; $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'][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'][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>0) { $strArr = self::mbStrSplit($title, $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) { imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $v); } else { $y = $y + ($fontSize * $k); imagettftext($editor, $fontSize, 0, $fontX + $the_box, $y, $colorResource, $fontPath, $v); } } return true; } else { return imagettftext($editor, $fontSize, 0, $fontX, $fontY, $colorResource, $fontPath, $text . $title); } } 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); // 智能计算最大宽度 $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); } } /** * 分割字符串为数组模式 * @param $string * @param $len * @return mixed */ private function mbStrSplit($string, $len = 1) { $start = 0; $strlen = mb_strlen($string); while ($strlen) { $array[] = mb_substr($string, $start, $len, "utf8"); $string = mb_substr($string, $len, $strlen, "utf8"); $strlen = mb_strlen($string); } return $array; } /** * 生成圆形图片 * @param static $imgpath 图片地址 * @param string $saveName 保存文件名,默认空。 */ private function circular($imgpath, $saveName = '') { $srcImg = imagecreatefromstring(file_get_contents($imgpath)); $w = imagesx($srcImg); $h = imagesy($srcImg); $w = $h = min($w, $h); $newImg = imagecreatetruecolor($w, $h); // 这一句一定要有 imagesavealpha($newImg, true); // 拾取一个完全透明的颜色,最后一个参数127为全透明 $bg = imagecolorallocatealpha($newImg, 255, 255, 255, 127); imagefill($newImg, 0, 0, $bg); $r = $w / 2; //圆半径 for ($x = 0; $x < $w; $x++) { for ($y = 0; $y < $h; $y++) { $rgbColor = imagecolorat($srcImg, $x, $y); if (((($x - $r) * ($x - $r) + ($y - $r) * ($y - $r)) < ($r * $r))) { imagesetpixel($newImg, $x, $y, $rgbColor); } } } // 输出图片到文件 imagepng($newImg, $saveName,0); // 释放空间 imagedestroy($srcImg); imagedestroy($newImg); } /** * 获取名片模板 * @return string * @throws \app\common\exception\BaseException * @throws \think\exception\DbException * @throws \Exception */ public function getImageE($config, $template_id = '') { $model = new Template(); $this->config = $config; // 小程序id $appId = $this->template['app_id']; // 1. 下载背景图 $backdrop = $this->saveTempImage($appId, $this->config['backdrop']['src'], 'backdrop'); // 2. 下载用户头像 $avatarUrl = $this->saveTempImage($appId, $model::$base_url."/image/agent/avatar.jpg", 'avatar', $template_id); // 2. 下载logo $logo = $this->saveTempImage($appId, $this->config['logo']['src'], 'logo'); // 4. 拼接名片 return $this->savePoster($backdrop, $avatarUrl, 'template', $logo); } /** * 手机号/座机号脱敏处理 * @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 * @return false|int */ private function colorResource($color, $editor) { $Color = new Color($color); list($r, $g, $b, $alpha) = $Color->getRgba(); $scale = round(127 * $alpha); $invert = 127 - $scale; return imagecolorallocatealpha( $editor, $r, $g, $b, $invert ); } /** *计算字体大小和顶部跟左边的距离 * @param $fontSize * @param $left * @param $top * @return array [fontSizePt, left, top] */ private function SizeLeftTop($fontSize,$left,$top){ // 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); } }