
1. 从一张海报说起imagettftext 为什么总是画歪做 PHP 图片文字生成的人大概率都踩过同一个坑imagettftext()一次只能画一行你给它一段中文它不会自己折行直接一路画到画布外面去。验证码、活动海报、商品水印、证书模板只要涉及多行文案就得自己算换行、自己控行高。核心检索词先摆出来PHP GD imagettftext 自动换行与行高设置本质是解决「一段 UTF-8 文本在指定像素宽度内如何按字符逐个测量、按行拆分、按行距落笔」的问题。它适合三类人一是用 GD 做验证码/水印的后端同学二是批量生成海报的运营工具开发者三是想给图片加多行说明文字、又不想引入 Imagick 的轻量项目。难点集中在三处。第一imagettfbbox()返回的坐标是相对基线算的$bbox[2]-$bbox[0]才是宽度很多人直接拿$bbox[4]当宽度结果换行位置全错。第二中文、英文、数字混排时逐字符测量比按词测量更稳因为中文没有空格分词。第三行高不是fontsizefontsize是字号行高是「上一行基线到下一行基线」的距离通常取fontsize * 1.4到fontsize * 1.8之间太小会挤太大会散。我试过最朴素的做法先按固定字数截断比如每行 10 个字。结果英文和数字宽度差异巨大一行 10 个「W」直接溢出一行 10 个「i」又空一大截。所以正确思路必须是动态测量每拼一个字就测一次当前行宽度超过限制就回退换行。下面这套流程就是把这个思路落成可复现的代码。2. TaoToken 统一 Key 通道把排版逻辑生成也纳入同一条链路写换行算法时最烦的不是代码本身而是边界情况标点不能出现在行首、英文单词不能从中间断开、数字和单位要绑在一起。这些规则用纯手写 if-else 会越写越长。我的做法是让模型帮我生成和补全这些排版规则而调用模型这件事统一走TaoToken的 Key 通道避免在项目里散落多个厂商的 Key 和 Base URL。TaoToken 在这里的角色很明确它是一个统一的 API 入口你用同一个 Key、同一个 Base URL就能调用不同模型来完成「生成 PHP 排版函数」「解释 imagettfbbox 返回值」「给出行高推荐值」这类任务。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置时别把查询串抄进去。为什么要在 GD 这种纯本地渲染的场景里引入模型因为排版规则是「语义级」的不是「像素级」的。像素级测量交给imagettfbbox()语义级规则哪些字符不能行首、哪些组合不能拆开交给模型生成两者分工。你拿到模型给的规则后仍然在本地用 GD 渲染验证模型不参与实际绘图这样既拿到了智能又保住了可控性。需要提前准备的东西一个可用的 TaoToken Key在控制台创建地址 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 一个支持中文的 TTF 字体文件比如NotoSansSC-Regular.ttf以及 PHP 7.4 且开启了 GD 扩展php -m | grep gd能看到 gd 即可。如果你还想让模型直接帮你写代码可以用模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 先试几轮确认输出风格再落到项目里。3. 可复制配置换行算法 行高参数 统一 Key 调用这一节给三段可直接粘贴的东西GD 换行函数、行高配置常量、以及调用 TaoToken 生成排版规则的请求体。先看换行函数。它接收画布、文本、最大宽度、起始坐标、字号、字体路径、颜色、行高、最大行数逐字符测量并落笔?php /** * GD 多行文字自动换行 * param resource|\GdImage $card 画布 * param string $str 待绘制文本UTF-8 * param int $width 单行最大像素宽度 * param int $x 起始 x * param int $y 首行基线 y * param float $fontsize 字号磅 * param string $fontfile 字体文件绝对路径 * param int $color GD 颜色句柄 * param int $rowheight 行间距额外增量像素 * param int $maxrow 最多绘制行数 * return int 实际绘制行数 */ function textWrapGd($card, string $str, int $width, int $x, int $y, float $fontsize, string $fontfile, int $color, int $rowheight 6, int $maxrow 5): int { $len mb_strlen($str, UTF-8); $line ; $row 0; $lineY $y; for ($i 0; $i $len; $i) { if ($row $maxrow) { break; } $char mb_substr($str, $i, 1, UTF-8); $try $line . $char; // 测量当前尝试行的像素宽度 $bbox imagettfbbox($fontsize, 0, $fontfile, $try); $tryW $bbox[2] - $bbox[0]; if ($tryW $width $line ! ) { // 超宽先落笔当前行再以该字符开新行 imagettftext($card, $fontsize, 0, $x, $lineY, $color, $fontfile, $line); $row; $lineY $fontsize $rowheight; $line $char; } else { $line $try; } } // 收尾最后一行 if ($line ! $row $maxrow) { imagettftext($card, $fontsize, 0, $x, $lineY, $color, $fontfile, $line); $row; } return $row; }行高参数建议单独抽成配置方便不同场景切换?php // config/typography.php return [ poster [fontsize 28, rowheight 14, maxrow 6], watermark [fontsize 16, rowheight 8, maxrow 3], captcha [fontsize 22, rowheight 10, maxrow 2], ];行高的经验值rowheight取fontsize * 0.5左右时视觉最舒服也就是 28 号字配 14 像素行距。如果你希望更紧凑降到fontsize * 0.3更松散升到fontsize * 0.8。注意这里的rowheight是「额外增量」实际行距等于fontsize rowheight。再给调用 TaoToken 生成排版规则的请求体用 curl 就能跑curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [ {role: system, content: 你是 PHP GD 排版助手只输出可执行代码不要解释。}, {role: user, content: 写一个 PHP 函数判断中文标点是否允许出现在行首返回布尔值。} ], temperature: 0.2 }如果你用 Claude Code 做长期开发可以把 Base URL 配成https://taotoken.net/apiKey 用同一个Model ID 填你选定的模型这样终端里的编码助手和项目里的 API 调用共用一套凭证。Coding Plan 入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 适合需要持续生成和迭代排版逻辑的场景。4. 验证请求本地渲染脚本跑出真实换行效果配置写完必须用真实图片验证不能只看代码逻辑。下面这个脚本生成一张 600x400 的画布写入一段中英混排文本输出 PNG 并打印实际行数?php require __DIR__ . /textWrapGd.php; $w 600; $h 400; $im imagecreatetruecolor($w, $h); $bg imagecolorallocate($im, 255, 255, 255); $fg imagecolorallocate($im, 30, 30, 30); imagefill($im, 0, 0, $bg); $text PHP GD imagettftext 自动换行测试这段文字包含中文、English words 和 12345 数字用来验证行高与换行位置是否正确。; $font __DIR__ . /fonts/NotoSansSC-Regular.ttf; $rows textWrapGd($im, $text, 520, 40, 80, 24, $font, $fg, 12, 8); echo 实际绘制行数: {$rows}\n; imagepng($im, __DIR__ . /out/wrap_test.png); imagedestroy($im);运行php render.php终端输出实际绘制行数: 4打开out/wrap_test.png你应该看到四行文字每行右边缘都控制在 520 像素以内行与行之间有明显但不拥挤的间距。如果行数变成 3 或 5说明宽度或行高参数需要微调。再验证一次模型辅助生成的规则是否可用。把第 3 节的 curl 请求跑一遍拿到返回的 PHP 函数后追加到项目里在换行前加一层判断如果当前字符是「。」且$line为空就把它并入上一行末尾。改完重新渲染对比图片标点不再顶到行首。成功结果的判断标准有三条一是所有行宽度不超过设定值二是行距均匀无重叠三是标点位置符合中文排版习惯。三条都满足这套配置就算跑通了。5. 常见报错排查401、local proxy failed 与 reading choices实际接入时报错基本集中在认证和响应解析两类。下面按真实错误信息对照排查。401 Unauthorized。最常见原因是 Key 没带上或带错。检查Authorization头是不是Bearer加空格再加 Key检查环境变量$TAOTOKEN_KEY是否真的导出echo $TAOTOKEN_KEY看有没有值。另一个隐蔽原因是把 API 地址写成了带 UTM 的官网地址正确写法是https://taotoken.net/api/v1/chat/completions路径里不要混入?utm_source这类参数。local proxy failed。这个报错通常出现在本地网络层不是 Key 的问题。先确认你的请求是直连taotoken.net没有经过任何本地转发配置。如果你在代码里设置了CURLOPT_PROXY把它去掉再试。PHP 里还要检查curl_setopt($ch, CURLOPT_PROXY, ...)是否被全局配置注入。reading choices 相关报错。典型信息是Cannot read properties of undefined (reading choices)或 PHP 侧Undefined index: choices。这说明响应体不是预期的 JSON 结构多半是请求被拦截返回了 HTML 错误页或者模型名写错导致返回了错误对象。排查方法先把响应原文打印出来var_dump($response)看第一层有没有choices字段。如果没有看error字段里的 message通常是模型 ID 不存在或参数格式不对。OAuth 相关报错。如果你用 Claude Code 或类似工具接入报OAuth token expired或invalid_grant说明走的是 OAuth 流程而不是 API Key 流程。改用 API Key 方式配置Base URL 填https://taotoken.net/apiKey 填控制台创建的 KeyModel ID 填具体模型名。这三件套Base URL Key Model ID缺一不可任何一项为空都会触发认证失败。GD 侧报错。imagettftext(): Could not find/open font说明字体路径不对用绝对路径别用相对路径。imagettfbbox(): Invalid font filename同理。如果中文显示成方框是字体本身不含中文字形换NotoSansSC或SourceHanSans这类中文字体。排障时建议把请求和响应都打到日志里尤其是 HTTP 状态码和响应前 200 个字符。很多「看起来像 Key 错」的问题实际是地址拼错或模型名写错。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 对照文档核对参数比反复试错快得多。6. 把 Key 通道固定下来排版逻辑就能持续迭代GD 的换行和行高本质是「测量—判断—落笔」三步循环代码不长但边界情况多。把测量交给imagettfbbox()把语义规则交给模型生成把调用入口统一到 TaoToken 的 Key 通道你的项目里就只需要维护一套凭证和一个 Base URL。后续无论是加「英文单词不断行」还是「数字与单位绑定」都只是往规则层加逻辑不用动认证层。长期做图片文字生成的话建议把模型调用封装成一个TypographyAssistant类内部固定https://taotoken.net/api和 Key对外只暴露suggestRowHeight()、fixPunctuation()这类方法。这样换模型、换参数都不影响业务代码。需要持续生成排版规则和编码辅助的可以从 Coding Plan 入口 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 进只想先验证模型输出质量的用模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 试几轮即可。