ThinkPHP 的二维码巡检图里,imagettftext 写入的中文标题突然变成一排小方块。这种乱码通常会被人怀疑是 PHP 的 GD 库没编译好,或者是文件编码不是 UTF-8,但实际上 InfoCodePhoto 的 generate_photo 里最容易被忽略的是 font_file 这个键。为了让 Codex 直接参与排查,我先在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 创建了 API Key,把 Codex 的模型通道切到 TaoToken(Base URL 填 https://taotoken.net/api),拿到 Key 后,它就能通过 TaoToken 走通模型通道,专心查 GD 文本绘制的问题。
1. 乱码在 InfoCodePhoto 的 imagettftext 调用链上
1.1 generate_photo 里中文标题经过的绘制路径
InfoCodePhoto 生成一张巡检图的流程并不复杂:imagecreatetruecolor 创建 1064 x 639 的画布,imagefill 填充白色底,deal_background 画顶部深蓝色区域,deal_logo 把 logo 和二维码原样贴上去,最后才是 imagettftext 写标题、提示文字和表格数据。问题往往出在最后一步,因为 imagettftext 需要一个真实的 TTF 字体文件来渲染字形。
判断标准很简单:如果「杆子光伏安全定点巡检二维码」这一行变成方块,而图片里的英文字母和数字还正常,说明 imagettftext 本身执行成功,问题出在字体文件缺少中文字形。如果整行中文完全消失,那大概率是 font_file 路径错误,GD 库加载字体失败后直接放弃了这行文本。这两种情况的原因和修法完全不同,不能混在一起处理。
1.2 配置数组里 font_file 的路径不一致是最大嫌疑
InfoCodePhoto 的默认配置里写了两处字体路径。类属性 self::$data["font_file"] 是ROOT_PATH . "public\\ttf\\" . "abd.ttf",而注释示例里写的是ROOT_PATH . "abd.ttf"。如果 generate_photo 被调用时没有通过 $origin_data 显式传入 font_file,程序最终用的是类属性里拼出来的那一条。
abd.ttf 这个文件名看起来像项目自己放的定制字体,它未必包含 CJK 统一表意文字的字形。很多开源字体只覆盖拉丁字符集,传到 imagettftext 后中文就映射不到任何字形,输出图片上自然是豆腐块。另一个隐蔽问题是目录分隔符:开发环境是 Windows 时反斜杠没问题,部署到 Linux 后public\ttf\abd.ttf会被当成一个非法路径,file_exists 直接返回 false。
2. 排查前先让 Codex 走 TaoToken 模型通道
2.1 从 TaoToken 官网创建 API Key
要请 Codex 加入排查,先得让它能稳定调用到模型。打开 TaoToken 完成注册,在控制台创建一个 API Key,创建后复制并保存为 YOUR_API_KEY。这个 Key 是在后面的 Codex 配置里作为环境变量使用的,不会写进项目代码。
提醒一下:落地页和接口地址是两码事。落地页 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 用来注册账号、创建 Key、查看模型广场和用量;真正要填进 Codex config.toml 的 Base URL 是 https://taotoken.net/api,末尾没有 /v1。两者混用会导致 Codex 把网页地址当作 API 入口,报出 404 或连接失败。
2.2 在 ~/.codex/config.toml 里写入 TaoToken 供应商
Codex 的自定义模型供应商写在 ~/.codex/config.toml,不要把它和 Claude Code 的 settings.json 混为一谈。Claude Code 用 ANTHROPIC_BASE_URL 环境变量,Codex 认的是 config.toml 里的 model_provider 配置。打开或创建这个文件,写入下面内容:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"其中 YOUR_MODEL_ID 不能靠自己猜,打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的模型广场,复制实际可用的模型 ID。env_key 指定的 TAOTOKEN_API_KEY 是 Codex 读取环境变量的名字,所以还要在终端里导出一次:
export TAOTOKEN_API_KEY="YOUR_API_KEY"把 YOUR_API_KEY 换成你在 TaoToken 创建的那串真实 Key。设置完成后,Codex 发起的模型请求会通过 TaoToken 的兼容通道走通,这一步是为了保证后续排查对话不被连接错误打断。
2.3 先让 Codex 跑一句与乱码无关的验证
不要一上来就把整个 InfoCodePhoto 类贴给 Codex,先确认模型通道本身是通的。在 Codex 会话里发起一句与业务无关的提问,比如询问 PHP 的 imagettftext 函数签名或 GD 库版本差异。如果 Codex 能正常回答,说明 Key、Base URL、模型 ID 这条链路没有问题,再进入代码排查环节。
3. 让 Codex 按这三处逻辑核对 generate_photo 中文绘制
3.1 font_file 的绝对路径与文件真实存在性
把 generate_photo 方法里和字体加载相关的代码摘出来,让 Codex 检查 ROOT_PATH 在项目里是怎么定义的。重点看拼出来的完整路径到底是ROOT_PATH . "abd.ttf"还是ROOT_PATH . "public/ttf/abd.ttf",并对比服务器上真实文件所在目录。与其在对话里反复猜,不如直接在本地命令行执行一句:
php -r '$font = __DIR__ . "/public/ttf/abd.ttf"; var_dump(file_exists($font), is_readable($font), filesize($font));'把执行结果贴给 Codex。file_exists 为 false 时,路径问题坐实;返回 true 但文件只有几十 KB,那这个字体大概率不是中文字体,需要换成覆盖 CJK 字形的开源字体。这时让 Codex 给出一个下载量较大的中文字体文件名,不要继续用 abd.ttf 硬扛。
3.2 imagettftext 参数里字体、坐标与颜色的覆盖关系
generate_photo 方法开头有一段很关键的覆盖逻辑:foreach 遍历 self::$data,只要 $origin_data 里存在同名键,就用新值替换默认值。这本来是为了灵活传参,但也带来了隐患。如果调用方在 $origin_data 里传入了 font_file 且值是空字符串,generate_photo 里的英文提示文字会照常显示,中文字符则全部静默失败。
让 Codex 对着这段代码检查 imagettftext 的四个关键调用点:标题绘制用 title_font_size 和固定坐标 (235, 102),温馨提示用 15 号字固定在 (680, 610),数据部分用 data_font_size 配合 $num 和 $count 计算坐标。如果标题和提示文字都乱码,而数据区域只有部分乱码,说明问题集中在分行截取,而不是字体本身。这个判断可以让 Codex 直接输出结论,再决定改字体还是改截取逻辑。
3.3 cn_row_substr 按字节分行是否切断中文
cn_row_substr 是 InfoCodePhoto 里专门做文本分行的函数,它按字节计算每行长度,默认每行显示 line_number 个字,每个中文字符占 3 字节。函数内部用 mg_cn_substr 以 3 字节为单位截取,逻辑上对纯中文没问题,但如果标题或表格值里混入了全角空格、生僻字或 emoji,这些字符可能占 3 到 4 字节,按固定 3 字节一截就会把 UTF-8 字符从中间劈开,生成图片上出现单个方框或问号。
让 Codex 特别检查设备编码这一行:代码里通过strpos($val["title"], "设备编码")把 line_number 改成 8,比默认的 12 更短。如果设备编码是「字母 + 数字 + 横线」的混合结构,每个字符占 1 字节,那么一行 8 个字符在图片上的实际宽度远小于中文 8 个字符的宽度,视觉上会跟其他行对不齐。这时需要让 Codex 基于 data_col_distance 和 data_start_x 算出每列的字符宽度上限,再反过来校验 line_number 是否合理。
4. 用本地 PHP CLI 重跑验证图片和调用记录
4.1 重跑 generate_photo 看输出图片
修改完 font_file 路径或换掉字体后,不要直接在浏览器里调接口,用本地 PHP CLI 跑一次更干净。因为浏览器输出图片时会出现 Content-Type 和调试信息混在一起的问题,CLI 环境能直接看到 warning 和 fatal error。执行:
php /path/to/generate_photo_test.php测试脚本里调用 generate_photo,传入巡检标题、表格数组、二维码路径和输出图片路径,然后检查输出图片是否存在。Codex 只能帮你生成和解释这段代码,实际执行要由你在本地完成,再把执行结果和报错信息贴回对话让它继续分析。重复两三轮后,图片上的中文标题从方块变成正常文字,说明 imagettftext 已经正确读到了中文字形。
4.2 回 TaoToken 控制台看 Codex 通道的调用记录
整个排查过程里,Codex 每次提问都会通过 TaoToken 通道产生一次模型调用。回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的用量页面,能看到刚才几轮诊断对话的请求次数和消耗情况,这比在多个 Key 之间来回切换更容易核算成本。
如果你把 font_file 换成服务器上真实存在的开源中文字体后,标题仍然出现个别方框,那就把新字体在本地生成的图片路径和 generate_photo 里对应的那几行贴给 Codex,让它按字节偏移量反推是哪个字符被 mg_cn_substr 切断。修完这一版,记得把 public/ttf 目录下的字体文件一起纳入版本库,避免新环境部署时再次出现同样的字体缺失问题。