PHP GD库动态生成圆形公章:环形文字与透明PNG实战解析
简介一整套用于在PHP项目中生成电子印章的自定义类代码直击合同盖章、证书签发、票据验签等场景中对圆形印章、五角星和环形文字绘制的需求适合需要为Web业务快速添加签章能力的PHP开发者参考。类基于GD库与imagettftext函数实现圆形印章绘制、五角星坐标计算、文本按角度旋转、字体加载等核心逻辑可直接在企业OA、电商证书、在线文档等系统中使用。压缩包共3个文件两个PHP源码文件包含印章类主体与可运行的调用示例一个simkai.ttf字体文件提供楷体字形支持整体大小2.24MB轻量易集成。已有849人学习下载适合PHP进阶开发者按需改造。通过示例代码可完整看到从创建Seal实例、设置sealColor、starAngle、textAngle等属性到绘制并输出PNG印章的调用链circleSeal.class.php中还对圆形轮廓、五角星颜色、文本旋转位置作了封装便于扩展透明度、尺寸自适应等更贴合实际业务的定制版。1. 为什么需要一枚能编程生成的公章PHP 印章类到底解决什么事我近几年拆过好几套合同审批系统几乎每个客户都提同一个需求帮我生成一枚公司公章要圆的、要红的、能盖到 PDF 上。手工做图不现实业务每次来一个新客户就换一次主体名称PS 改字更是不敢碰改错一个字就得返工。PHP 印章类就是干这个的在服务器端用 PHP 动态绘制圆形或椭圆形印章输出透明底 PNG再叠加到制度文件、授权书和电子合同上。如果你是做 OA、审批流、电子签这类系统的从业者又不想依赖绘图软件或外包设计这套资源能直接拿参数复用。下面把生成原理、核心计算和踩过的坑一次说透。2. GD 库绘图与字体选型为什么这类资源几乎都用 GD 而不是 Imagick2.1 为什么优先选 GD印章的二维图形需求刚好落在 GD 的舒适区印章生成本质上就是画圆、写字、填充多边形这几种基础图形操作GD 库全部覆盖而且它是 PHP 官方内置扩展绝大多数发行版默认开启虚拟主机环境下也能跑。Imagick 虽然图像处理能力强但它依赖额外的 ImageMagick 二进制容器化环境里经常因为缺少动态库装不上我在某公司线上环境就被这个坑过一次最后只能换回 GD。SVG 方案表达圆形文字确实更优雅但服务端如果要输出 PNG 给前端直接用最终还是得转栅格图绕一圈又回到 GD 或 ImageMagick。所以实战里我优先把 GD 作为默认方案只有需要复杂滤镜、批量转换几百页 PDF 时才考虑 Imagick。选型结论一句话先用 GD 把业务跑通遇到解决不了的问题再换引擎。2.2 字体文件准备仿宋、宋体与 TTF 路径的常见误区中文印章的字体选择很有讲究。公章样式默认偏向仿宋体没有仿宋就退而求其次用宋体两者在 GD 里渲染出来的笔锋差异直接决定印章是否像真的。很多人在这一步就翻车因为官方文档只给参数不强调字体实际项目里找不到合适的字体文件是常态。先在服务器上检查系统已有的中文字体fc-list | grep -iE fangsong|simsun|songti如果输出为空就需要手动上传字体文件。我一般会把simfang.ttf放到/usr/share/fonts/chinese/目录下然后刷新字体缓存mkdir -p /usr/share/fonts/chinese cp simfang.ttf /usr/share/fonts/chinese/ fc-cache -f这里有一个非常容易踩的坑GD 对.ttc格式的兼容性并不稳定。老版本的 GD 库认不出 TrueType Collection而 Windows 系统自带的msyh.ttc就是这种格式直接拿它渲染会出现无法打开字体文件的报错。所以我坚持只用单独的.ttf文件上传之前用file simfang.ttf看一眼格式确认没问题再使用。2.3 画布与透明背景imagecreatetruecolor 的两条铁律创建画布时有两条铁律违反任何一条都会让印章成品率暴跌。第一必须用imagecreatetruecolor而不是imagecreate。后者是 256 色调色板模式画出来的圆环边缘会有明显锯齿和色块断层稍微放大一点就没法看。第二印章必须有透明背景因为后续要叠加到扫描件或合同纸上白底方块盖上去既遮挡原文件又显得假。透明背景的设置顺序很关键先关混合再存 alpha最后填充透明色。顺序反了透明区域会变成黑色?php $size 300; $im imagecreatetruecolor($size, $size); // 关键先关混合再保存 alpha 通道最后填充全透明色 imagealphablending($im, false); imagesavealpha($im, true); $transparent imagecolorallocatealpha($im, 0, 0, 0, 127); imagefill($im, 0, 0, $transparent); ?这段代码里imagealphablending($im, false)是告诉 GD 后续绘图操作不要再做 alpha 混合否则透明底会被后续的红色文字和边框覆盖成半透明状态imagesavealpha($im, true)让 PNG 输出时保留 alpha 通道信息imagecolorallocatealpha的第四个参数 127 表示完全不透明度的反向值127 对应完全透明0 对应完全不透明。这里用 127 填充背景就得到一张干净的全透明画布。常见错误是直接imagefill($im, 0, 0, 255)填充白色再写字最后 PNG 是白底方块盖到深色合同上非常突兀。还有一些实现里先画了红色圆环才去关混合导致圆环边缘出现黑边这些都是把透明通道作为事后补救才会遇到的问题。3. 把公章画出来环形文字、五角星与双圆边框的核心计算3.1 环形文字先量宽度再旋转别按字符数等分圆形印章最复杂的就是文字绕圆环排列。很多新手实现按字符总数平均分配 360 度结果就是窄字符间距大、宽字符挤在一起看着极其不专业。正确做法是先用imagettfbbox测量每个字符的实际像素宽度再按弧长换算成旋转角度。核心逻辑是把字符串拆成单字测量总宽度然后按比例分配每个字符在圆周上的角度。GD 坐标系里 y 轴向下的特性还要额外处理不然文字会逆时针排列?php $chars mb_str_split($company_name); $totalWidth 0; // 先测量整串文字的实际像素宽度 foreach ($chars as $c) { $bbox imagettfbbox($fontSize, 0, $fontPath, $c); $totalWidth ($bbox[2] - $bbox[0]) $charSpacing; } $radius $radiusCenter; // 文字所在圆环半径 $baseAngle -90; // 从正上方开始角度负向为顺时针 foreach ($chars as $c) { $bbox imagettfbbox($fontSize, 0, $fontPath, $c); $charWidth $bbox[2] - $bbox[0]; $theta ($charWidth / $radius) * 180 / M_PI; // 当前字符中心对应的弧度位置 $radian ($baseAngle - $theta / 2) * M_PI / 180; $x $cx $radius * cos($radian) - ($bbox[2] $bbox[0]) / 2; $y $cy - $radius * sin($radian) - ($bbox[5] $bbox[1]) / 2; // 字符自转角度 当前角度 - 90 度保证字头朝向圆外 imagettftext($im, $fontSize, $baseAngle - 90, $x, $y, $color, $fontPath, $c); $baseAngle - $theta $spacingAngle; } ?这里用 0 度测量每个字符宽度是为了拿到准确的包围盒$theta把像素宽度换算成当前半径下的角度增量字符的绘制起点用包围盒中心校准避免中文的上下引线导致整体偏移。imagettftext的自转角度用$baseAngle - 90是因为 GD 的 0 度水平向右而圆环切线方向需要顺时针旋转 90 度才能让字头朝外。这个算法看起来简单参数没调好就会出现字符逆时针、字头朝内或相邻字符重叠。3.2 五角星十边形顶点交替连线内半径用 0.382 倍圆形公章下沿中间要嵌一个五角星。五角星的数学本质是十个顶点交替取内外半径外顶点间隔 72 度内顶点嵌在中间内半径取外半径的 0.382 倍。这个比例的来源是正五角星的凹点半径0.382 正好等于sin18° / sin54°画出来比例最协调。?php $points []; $outerR $radius * 0.35; // 五角星外顶点半径相对印章半径约 0.3~0.35 $innerR $outerR * 0.382; // 内顶点半径黄金分割相关比例 for ($i 0; $i 10; $i) { $isOuter ($i % 2 0); $rr $isOuter ? $outerR : $innerR; $angleDeg -90 $i * 36; // 从正上方开始每 36 度一个顶点 $angleRad $angleDeg * M_PI / 180; $points[] $cx $rr * cos($angleRad); $points[] $cy $rr * sin($angleRad); } imagefilledpolygon($im, $points, 10, $red); ?这里用-90度作为起始角让第一个外顶点指向正上方这样五角星是端正的不会歪向左边。imagefilledpolygon会自动闭合图形所以只需要给 10 组坐标。需要注意外半径不能超过内圈半径的 0.4 倍否则五角星的凹点会压到文字环上也不能小于 0.25 倍太小了看起来像个小圆点。3.3 双圆边框与防伪纹理线宽、圆环间距和随机噪点印章的边框分三层结构最外层粗圆环、中间细圆环、最内侧的微调圆。外圆线宽通常 6 到 10 像素内圈线宽 2 到 3 像素两者间距大约 4 到 8 像素。用imagesetthickness控制线宽但要记住这个函数影响的是之后所有的绘制操作画完圆环必须把它重置回来否则五角星的边框会变粗甚至填充异常。?php // 先画外粗环 imagesetthickness($im, 8); imageellipse($im, $cx, $cy, $size - 4, $size - 4, $red); // 再画内细环 imagesetthickness($im, 2); imageellipse($im, $cx, $cy, $size - 30, $size - 30, $red); // 画完必须重置线宽 imagesetthickness($im, 1); ?imageellipse前两个参数是圆心坐标后两个是椭圆宽度和高度。圆形印章传相同值即可。这里$size - 4留出 2 像素边距避免圆环顶到画布边缘。如果画出来的圆环看起来偏斜先检查圆心是否在画布中心而不是马上怀疑笔画问题。防伪纹理是印章看起来真实的最后一环。真实公章表面有细微的十六角防伪纹和随机蚀刻纹GD 里用随机噪点模拟即可密度不要太高否则打印出来一团黑?php $noiseDensity 0.002; for ($x 3; $x $size - 3; $x) { for ($y 3; $y $size - 3; $y) { if (mt_rand(1, 1000) / 1000 $noiseDensity) { imagesetpixel($im, $x, $y, $red); } } } ?噪点密度参数化到配置里默认 0.002 表示每像素有千分之二概率被着色一枚 300 像素见方的印章大约会生成 160 个随机点。做批量生成时调低到 0.001 可以显著降低渲染耗时。4. 实例化一枚公司公章参数表、调用顺序与 PNG 输出4.1 配置参数表从公司名到噪点密度全列清楚印章类应该做成纯配置驱动每个客户只需要改一组参数就能生成自己的章。我常用的配置项如下参数类型默认值说明company_namestring无章面单位名称必须 UTF-8 编码radiusint150印章外圆半径建议 150~300font_sizeint28环形文字字号按名称长度调整font_pathstring/usr/share/fonts/chinese/simfang.ttf仿宋字体绝对路径ring_widthint8外圆环宽度像素inner_ring_widthint2内圆环宽度像素star_radius_factorfloat0.35五角星外半径占印章半径的比例colorarray[200, 20, 20]印章主色 RGBnoise_densityfloat0.002防伪噪点密度char_spacingint2环形文字间距像素radius决定画布大小我一般取$size $radius * 2这样后续所有坐标都以($radius, $radius)为中心计算。star_radius_factor控制五角星视觉大小别用固定像素值否则换了画布尺寸五角星就变形。4.2 一个完整可跑的生成示例下面是一个最小可用实现包含圆形公章生成的完整流程创建透明画布、绘制双环、排列环形文字、画五角星、输出 PNG。?php class CompanySeal { private $config; public function __construct(array $config) { $this-config $config; } public function render() { $cfg $this-config; $radius $cfg[radius]; $size $radius * 2; $cx $cy $radius; $im imagecreatetruecolor($size, $size); imagealphablending($im, false); imagesavealpha($im, true); $transparent imagecolorallocatealpha($im, 0, 0, 0, 127); imagefill($im, 0, 0, $transparent); // 颜色 [$r, $g, $b] $cfg[color]; $red imagecolorallocate($im, $r, $g, $b); // 双圆环 imagesetthickness($im, $cfg[ring_width]); imageellipse($im, $cx, $cy, $size - 4, $size - 4, $red); imagesetthickness($im, $cfg[inner_ring_width]); imageellipse($im, $cx, $cy, $size - 30, $size - 30, $red); imagesetthickness($im, 1); // 环形文字 $chars mb_str_split($cfg[company_name]); $totalWidth 0; foreach ($chars as $c) { $bbox imagettfbbox($cfg[font_size], 0, $cfg[font_path], $c); $totalWidth ($bbox[2] - $bbox[0]) $cfg[char_spacing]; } $baseAngle -90; foreach ($chars as $c) { $bbox imagettfbbox($cfg[font_size], 0, $cfg[font_path], $c); $charWidth $bbox[2] - $bbox[0]; $theta ($charWidth / ($radius * 0.85)) * 180 / M_PI; $radian ($baseAngle - $theta / 2) * M_PI / 180; $x $cx $radius * 0.85 * cos($radian) - ($bbox[2] $bbox[0]) / 2; $y $cy - $radius * 0.85 * sin($radian) - ($bbox[5] $bbox[1]) / 2; imagettftext($im, $cfg[font_size], $baseAngle - 90, $x, $y, $red, $cfg[font_path], $c); $baseAngle - $theta 2; } // 五角星 $points []; $outerR $radius * $cfg[star_radius_factor]; $innerR $outerR * 0.382; for ($i 0; $i 10; $i) { $rr ($i % 2 0) ? $outerR : $innerR; $ang (-90 $i * 36) * M_PI / 180; $points[] $cx $rr * cos($ang); $points[] $cy $rr * sin($ang); } imagefilledpolygon($im, $points, 10, $red); return $this-output($im); } private function output($im) { ob_start(); imagepng($im); $data ob_get_clean(); imagedestroy($im); return $data; } } // 使用示例 $seal new CompanySeal([ company_name 某某科技有限公司, radius 150, font_size 26, font_path /usr/share/fonts/chinese/simfang.ttf, ]); file_put_contents(/tmp/seal.png, $seal-render()); ?这里环形文字半径取了$radius * 0.85留出外环和内圈之间的空间具体值要根据字号微调字号越大文字越高环形半径就要越靠近外圈。mb_str_split是 PHP 8 的函数PHP 7 环境可以用preg_split(//u, $name, -1, PREG_SPLIT_NO_EMPTY)代替。4.3 输出 PNG 与透明通道imagepng 和输出缓冲的配合直接imagepng($im)会输出图片内容并返回布尔值如果你在框架里想拿到二进制内容再二次处理会被返回的true干扰。我用ob_start()加ob_get_clean()实现内容缓冲把 PNG 数据作为字符串返回调用方想存文件、想 base64 编码、想直接输出到响应都方便。这里有一个框架开发常见的坑如果前面有模板引擎或自动加载器输出了空行或 BOM 头只要在输出之前调用一次ob_clean()清掉历史缓冲再imagepng($im)就能避免“图片无法预览”的尴尬。4.4 叠加到合同图上的两个常见误用生成印章只是第一步真正业务是把印章盖到合同扫描件上。用imagecopymerge直接叠透明 PNG 是最常见的误用这个函数对 alpha 通道的处理非常粗糙透明区域会被填充成黑色或白色边缘锯齿明显。我一般会先建一个与底图同尺寸的临时画布填白色把透明章压上去再用imagecopymerge($doc, $tmpImg, $x, $y, 0, 0, $w, $h, 100)压到最终文档上。这样虽然多一步但边缘过渡干净不会出现黑边。另一个误用是直接用imagecopy它完全不处理透明度透明像素会被当作纯黑色处理整个章会变成一个黑块。5. 避坑与排查印章生成最容易翻车的五个现场5.1 环境类踩坑GD 扩展未开启、FreeType 缺失现象调用imagettftext直接报错Call to undefined function imagettftext()或者生成出来的图片里文字缺失只剩圆环和五角星。原因GD 扩展没有安装或者安装了 GD 但编译时没有启用 FreeType 支持。imagettftext依赖 FreeType 库渲染 TrueType 字体很多精简版 PHP 镜像默认不开启。解决先检查运行环境。php -m | grep gd php -i | grep -i freetype如果输出为空安装并启用扩展apt install php-gd # 或宝塔面板软件商店 → PHP → 安装扩展 → gd装完重启 PHP-FPM 再试。我遇到过一种情况是 CLI 模式的php -m有 gd但 FPM 模式的扩展路径不同排查时要两个都确认不能只信一个。5.2 字体类翻车中文变方块、路径和编码问题现象文字渲染出来是整排小方块或者生成过程直接报Invalid font filename。原因一是字体路径写错file_exists检查没做二是字体文件是.ttc格式老版本 GD 不支持三是传入的$company_name是 GBK 编码GD 只认 UTF-8。解决统一做三件事。路径先file_exists验证.ttc文件转成.ttf或用单独的simfang.ttf字符串强制mb_convert_encoding($name, UTF-8, GBK)。我习惯在构造函数里加一道过滤遇到非法路径直接抛出异常避免运行时才发现字体不对。5.3 绘制类翻车环形文字跑飞出圆、五角星中心偏移现象文字没有沿圆环排列而是跑到画布外或者五角星明显偏左不在中心。原因文字跑飞是因为按字符数等分角度而不是按实际宽度换算五角星偏移是因为圆心用了($size/2, $size/2)但印章半径不是$size/2比如把外圆半径设成 130 而画布大小按 300 创建中心就偏了。解决文字部分用 3.1 的测量宽度算法先量后画五角星圆心统一定义为$cx $cy $radius所有圆环、文字、星形都以这个坐标为中心。我曾经在某项目里因为少写一个括号导致五角星整体偏移两个像素肉眼看不出来但叠加到高清扫描件上一眼就能看出章没盖正。5.4 性能类问题高并发下内存暴涨、字体重复加载现象批量生成 100 枚章耗时几十秒并发上来直接超时或内存溢出。原因画布默认分配 4 字节每像素600 像素见方的画布约 1.4MB本身不大但每次请求都重复加载字体文件、创建销毁画布并且imagedestroy没被调用时 PHP 要等请求结束才释放。解决把画布控制在 300 到 500 像素输出到 600 像素的需求交给imagepng的缩放或者前端 CSS内存中复用同一个画布循环里重新填充透明色避免每枚章都重新imagecreatetruecolor调完立即imagedestroy。字体路径用类静态变量缓存避免每次new都重新读盘。5.5 输出类问题保存为 JPG 导致透明底变黑现象远程图片预览一团黑底或者样式表里设置圆角却看到黑色角块。原因JPG 不支持透明度透明像素在转换时变成黑色或者输出时忘记调用imagesavealpha($im, true)导致 alpha 通道根本没写入 PNG。解决一律输出 PNG开发阶段每次渲染完都检查一下文件头PNG 的十六进制头是89 50 4E 47如果开头变成FF D8 FF就是被转成了 JPG。实际部署时我会在图床上传接口里加一道 MIME 校验拦截非 PNG 文件。6. 进阶技巧批量生成与把印章合成到 PDF 合同上6.1 批量生成与文件名策略真正项目里不会只生成一枚章而是为一堆子公司、部门批量生成。循环调用CompanySeal类时文件命名不要直接用公司名。中文字符在文件系统里容易遇到编码问题Windows 与 Linux 之间拷贝还会乱码。我用md5($companyName)生成文件名另存一张公司名与哈希的映射表这样既避免重名冲突又不泄露主体信息。?php $list [[name 某贸易公司], [name 某科技公司]]; foreach ($list as $row) { $seal new CompanySeal([ company_name $row[name], radius 150, font_size 26, font_path $cfg[font_path], ]); $filename md5($row[name]) . .png; file_put_contents($dir . / . $filename, $seal-render()); } ?批量渲染时建议每生成完一枚就gc_collect_cycles()一次GD 资源在循环里释放不及时数量一多内存会慢慢涨上去。6.2 把印章写到 PDF 合同上坐标、尺寸与透明度PHP 生成 PDF 常用 TCPDF 或 FPDF。这两种库的Image方法对 PNG 透明通道支持良好不需要像 GD 那样做白底预合成。?php $pdf new TCPDF(P, mm, A4, true, UTF-8, false); $pdf-AddPage(); // 印章 PNG 绝对路径$x/$y 为盖章位置的左上角坐标单位毫米 $pdf-Image(/tmp/seal.png, $x, $y, 40, 40); ?实际盖章位置通常要覆盖在落款单位名称之上$x和$y需要根据正文排版动态计算而不是写死坐标。我一般把 A4 页面宽度 210mm 减去印章宽度 40mm得到右侧区域再按落款行的垂直位置微调。TCPDF 的Image方法会保留 PNG 的 alpha 通道放大到 60mm 也清晰直接生成 PDF 就不需要再经过 GD 合成底图。有一回我调这个类调到半夜最后发现不是算法问题而是字体路径里多了一个空格。从那以后我每次部署完都会强制走一遍完整链路先跑一行测试数据生成 PNG再确认透明通道最后嵌入一张测试 PDF 验证坐标。这套流程看着笨但真的能在一分钟里定位九成问题。希望帮到你。本文还有配套的精品资源点击获取