资讯详情

Java2D自建电子签章生成与PDF盖章实战:从绘制到合成

📅 2026/10/10 14:47:05 | 华诺云谱 👁 阅读
Java2D自建电子签章生成与PDF盖章实战:从绘制到合成
简介面向需要实现电子合同签章的Java开发者这份zip压缩包基于Spring Boot框架给出了一套从PDF文档处理到数字签名生成与应用的完整实现思路。压缩包大小约72KB文件总数及类型明细暂无数据可结合项目结构直接学习核心代码与配置。内容涉及Spring Boot微服务搭建、RSA与DSA等非对称加密算法应用以及利用iText或Apache PDFBox对PDF合同的读写和指定位置插图同时借助JCA、JCE与Bouncy Castle完成数字签名的生成与验证确保合同完整性和来源可信符合电子合同场景的安全要求。资源覆盖Maven项目中的pom.xml依赖配置、主程序与业务逻辑类、resources下的证书、密钥及电子章图片存放方式适合具备一定Java基础、希望快速落地电子签章功能的开发者参考。目前已有1249人学习下载对理解数字签名原理以及Spring Boot集成PDF处理具有直接的借鉴价值。1. 电子签章生成这件事为什么值得自己做一遍电子签章这个需求几乎所有做合同管理系统的人都会撞上。我最早接触它是在一套内部合同审批系统里业务方要求每份合同自动生成带红色公章的 PDF同时数据不能出公司内网。去问第三方电子签厂商按调用次数收费还要求合同文件走他们的云服务领导一听就打回。后来我把方案换成自建用 Java2D 绘制红色公章再按坐标贴合到合同 PDF 上。这套自建方案没有外部网络调用公司名、印章类型、落款位置全部走参数配置签章图是实时渲染出来的不是切好的素材图新客户接入时不用重新做图片。它适合手头有一批合同要做电子签章的 Java 后端开发也适合被商业 SDK 报价劝退、想弄明白签章原理的人。2. Java2D 绘制电子签章圆弧文字、五角星与透明画布拆解电子公章的核心结构其实比想象中简单拆开看就是四部分外圈圆环、上弧的公司全称、下弧的印章类型文字、中心的五角星。部分印章还会在五角星下方加一串 13 位防伪编码但那是后话。把结构拆清楚之后Java2D 的绘制工作就非常直接了——本质是在一张透明图片上用 Graphics2D 画圆、写字、画五角星最后输出 PNG。真正有技术含量的地方只有三处透明画布怎么建、汉字怎么沿着圆弧排、五角星的坐标怎么算。2.1 为什么用代码实时绘制而不是直接用设计好的图片我见过很多项目的做法是找设计做一张公章 PNG然后嵌到 PDF 里。这种方案短期能用但有几个问题绕不开图片放大到打印尺寸会发虚公司名改动一次就要重新做图更麻烦的是素材图背景抠不干净盖到 PDF 上总有一圈白边。用 Java2D 实时绘制公司名称是构造参数改一次配置就能重新生成一枚章画布大小可以按打印需求放大到 1200 甚至更高边缘锯齿还能通过抗锯齿设置抹平。先看最基础的画布创建这是后面所有绘制的前提BufferedImage image new BufferedImage(600, 600, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d image.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2d.setColor(new Color(178, 34, 34)); // 国标红这段代码的逻辑是先创建一张 600x600 的 ARGB 画布再拿到 Graphics2D 对象开启抗锯齿后设置绘制颜色。注意两点。第一TYPE_INT_ARGB的 A 就是 Alpha 透明通道选了它背景才是透明的如果手滑用了TYPE_INT_RGB后面输出 PNG 也只能得到黑底或白底这是生成签章最常见的翻车点之一。第二颜色值 178,34,34 是接近公章的国标红比纯红 255,0,0 更沉稳打印出来也不会刺眼。g2d的setRenderingHint必须显式开启尤其文字抗锯齿如果不开印章上的小字号汉字在横向笔画上会非常毛糙。2.2 汉字沿圆弧排列逐字旋转比 GlyphVector 更适合起步上弧和下弧的文字排列是整个生成器里最容易写错的地方。常见实现有两种一种是用GlyphVector拿到每个字的轮廓再把轮廓沿圆弧变换精度高但代码量大另一种是逐字计算角度和位置再用AffineTransform把字旋转到对应方向代码短且肉眼效果足够。我一般用逐字旋转的方案因为印章字体是固定的仿宋或宋体不像艺术字那样需要精确曲线贴合。private void drawArcText(Graphics2D g2d, String text, double cx, double cy, double radius, boolean top, double startAngle) { FontMetrics fm g2d.getFontMetrics(); double textWidth fm.stringWidth(text); double radianPerChar textWidth / radius / text.length(); double theta top ? -Math.PI / 2 : Math.PI / 2; for (int i 0; i text.length(); i) { String ch String.valueOf(text.charAt(i)); double chWidth fm.stringWidth(ch); double chAngle chWidth / radius; if (top) { theta chAngle / 2; } else { theta - chAngle / 2; } double x cx radius * Math.cos(theta); double y cy - radius * Math.sin(theta); AffineTransform old g2d.getTransform(); g2d.translate(x, y); double rotate top ? theta Math.PI / 2 : Math.PI / 2 - theta; g2d.rotate(rotate); g2d.drawString(ch, -chWidth / 2, fm.getAscent() / 2); g2d.setTransform(old); if (top) { theta chAngle / 2; } else { theta - chAngle / 2; } } }这段代码的逻辑是先把整个字符串宽度除以半径得到一个平均弧度步长然后从 12 点方向-π/2开始逐字累加角度。每个字符的绘制坐标由centerX radius * cos(theta)和centerY - radius * sin(theta)算出再通过g2d.rotate把字符旋转到沿圆弧的切线方向。参数里top控制文字在上弧还是下弧上弧文字正向排列公司名从左往右顺着圆弧走下弧文字反向排列印章类型文字从右往左这样人眼从正面看才是正的。startAngle用于微调起点比如公司名称字数太多或者太少时可以让整行文字整体偏移一点视觉上更居中。我调这个函数时花了不少时间在两个字符间距上。如果直接用固定角度步长公司名最后几个字会挤到一起因为中文全角字符的charWidth并不完全一致。上面代码里把每字符实际宽度除以半径再换算成弧度宽度不一致的中文也能自动调整间距。如果要求更严格可以用GlyphVector的getGlyphPositions()拿精确轮廓但合同章场景下肉眼几乎分辨不出差别。2.3 五角星与圆环Path2D 画星、Stroke 画环五角星用Path2D.Double绘制核心是五个外顶点和五个内顶点交替连接。外顶点落在半径为 rOuter 的圆上内顶点落在半径为 rInner 的圆上两圆半径之比决定五角星的胖瘦。做法是循环 10 次偶数下标用外半径、奇数下标用内半径角度从顶部开始每隔 36 度取一个点。private void drawStar(Graphics2D g2d, double centerX, double centerY, double rOuter, double rInner) { Path2D.Double star new Path2D.Double(); for (int i 0; i 10; i) { double angle Math.PI / 2 - i * Math.PI / 5; double r (i % 2 0) ? rOuter : rInner; double x centerX Math.cos(angle) * r; double y centerY - Math.sin(angle) * r; if (i 0) { star.moveTo(x, y); } else { star.lineTo(x, y); } } star.closePath(); g2d.fill(star); }这里的参数含义要理解透rOuter是五角星外圆半径一般取印章内径的十分之一到八分之一rInner是内圆半径取外半径的 0.38 到 0.5 之间太小了星形又尖又长太大了会接近实心圆。Math.PI / 2 - i * Math.PI / 5这个角度表达式的意思是从正上方表盘 12 点方向开始每 36 度取一个顶点10 个点正好绕一圈。圆环的绘制更简单g2d.drawOval加一个粗一点的Stroke就能实现但要注意外圆线宽和次圆线宽要有层次感。我习惯外圆线宽 4px内部第二道圆环线宽 1.5px内圈文字半径在外圆半径基础上向内缩 28px 到 36px这样文字和外环之间留出一条视觉间隔不会糊在一起。3. 从参数到文件签章生成器的输入输出与映射规则画布和基础图形写完之后下一步是把绘制逻辑包装成可以直接调用的生成器。这一步决定这个工具是否好落地重点在于印章类型映射、上下文参数设计、以及输出透明 PNG 时对 Alpha 通道的检查。很多初学者把绘制代码写好就往 Spring Boot 里塞结果公司名、印章类型、尺寸全写死在业务代码里换一个客户就要改一次代码这是没有做好参数抽象的表现。3.1 印章类型与参数映射电子签章不是只有一种红章。合同专用章、公章、财务专用章的形状和内容并不相同财务章通常是椭圆而不是正圆合同章的下弧文字固定是「合同专用章」公章下弧可能什么都不写或者写公司编号。我建议在生成器里先定义枚举SealType再把每种类型的绘图参数集中到一个映射表里印章类型形状长宽比中心五角星内圈文字典型画布尺寸公章正圆1:1有公司名称600x600合同专用章正圆1:1有合同专用章600x600财务专用章椭圆0.9:1无财务专用章540x600映射规则写在一个SealContext类里生成器只依赖这个上下文对象不直接依赖业务参数。椭圆章的实现也不复杂只需要在绘制圆环和计算文字圆弧时把同一个半径按长宽比拆成radiusX和radiusY圆弧文字计算用椭圆积分会非常麻烦实际做法是把椭圆近似成等周长圆用等距半径来排字肉眼看不到明显拉伸。3.2 一个可以直接跑的生成入口生成器对外暴露的方法不用多一个generateToFile和一个generateToBytes就够。前者用于本地验证后者用于在 Web 服务里直接读字节流返回给前端预览。演示代码如下public class SealDemo { public static void main(String[] args) throws Exception { SealContext context new SealContext() .setCompanyName(某某科技有限公司) .setSealType(SealType.CONTRACT) .setCanvasSize(600); SealGenerator generator new SealGenerator(context); File sealFile generator.generateToFile(seal.png); System.out.println(生成印章文件 sealFile.getAbsolutePath()); } }这段代码的逻辑很好理解先构建一个SealContext配置公司名称、印章类型和画布尺寸然后实例化SealGenerator调用generateToFile落盘 PNG。参数说明companyName会渲染到印章上弧如果公司名超过 12 个字字体大小会自动缩小否则文字会溢出到圆环外SealType.CONTRACT会触发映射表里合同专用章的配置下弧文字自动设为「合同专用章」并开启中心五角星canvasSize是生成图片的像素尺寸600 适合屏幕预览和普通打印如果合同要高清印刷建议直接设为 1200反正 Java2D 渲染矢量文字的耗时可以忽略。生成器内部封装的事情包括根据SealType选择圆形或椭圆绘制逻辑、字号随字数和半径自动调整、调用drawArcText画上下弧文字、调用drawStar画五角星。这一层封装做完业务方调用时就不需要关心任何绘图细节。3.3 输出 PNG透明背景与 Alpha 通道验证输出文件这部分容易踩一个隐蔽的坑ImageIO.write的格式参数决定是否能保留透明通道。JPEG 是不支持透明通道的无论画布是不是 ARGB输出 JPEG 时 Alpha 信息直接丢失之前画的印章背景会变成黑色。所以保存文件必须指定为pngImageIO.write(image, png, outFile); boolean hasAlpha image.getColorModel().hasAlpha(); System.out.println(图片是否包含透明通道 hasAlpha);逻辑说明ImageIO.write的第一个参数是BufferedImage第二个参数是图片格式字符串第三个是输出目标。image.getColorModel().hasAlpha()返回 true说明这张图能正确保留透明信息。参数说明如果你的生成流程里有一处把BufferedImage转成了TYPE_INT_RGB那么hasAlpha()会返回 false这时候不要继续往 PDF 里贴图了先回头检查画布创建否则贴上去的是一块带黑底的方图。如果要把生成器接到 Spring Boot 里常见做法是写一个SealService把SealContext换成请求参数generateToBytes返回byte[]再用ResponseEntity输出给前端。Java 后端做这套没有任何额外依赖不需要引入第三方图形库JDK 自带的java.awt和javax.imageio就够。4. 把签章盖进 PDF坐标换算、批量落款与合规性注意印章 PNG 生成之后真正交付给业务的是一份带红章的 PDF 文件所以 PDF 合成是整个链路里绕不过去的一步。这一步的技术难点不是「能不能贴图」而是「贴在哪里、贴多大、如何批量处理」。把图往 PDF 上贴不只有drawImage这一个动作还涉及 PDF 页面的坐标原点方向、页面尺寸获取、以及打印时的 DPI 换算。这些细节处理不好PNG 再精致也白搭。4.1 为什么用 Apache PDFBox 而不是 iTextJava 生态里操作 PDF 最常用的两个库是 iText 和 PDFBox。iText 功能确实更强大支持表单填写、数字签名等高级特性但 iText 5.0 之后采用了 AGPL 和商业授权双轨制公司内部系统如果不想开源就需要购买商业授权。PDFBox 是 Apache 2.0 许可证不需要开源你的业务代码做公司内部工具完全够用。我在代码里用的是 PDFBox 2.0.27Maven 坐标如下dependency groupIdorg.apache.pdfbox/groupId artifactIdpdfbox/artifactId version2.0.27/version /dependency选型理由一句话功能够用且没有授权隐患。对电子签章这个场景来说需要用到的 PDFBox 能力只是加载 PDF、定位页面、画图像、保存这些 API 非常稳定社区资料也全。4.2 落款页定位与坐标换算原点在左下角PDF 的坐标系和常见图像处理工具不一样原点在页面左下角x 向右为正、y 向上为正单位是 pt1/72 英寸。对于一张 A4 纸MediaBox的宽高通常是 595 x 842 pt。很多人在这一步翻车是因为习惯用屏幕左上角的坐标系直接把章贴在了页面外或者叠到了页眉位置。贴章代码的核心逻辑分三步取最后一页作为落款页、确定盖章宽度、按目标位置计算 x 和 y。示例代码如下try (PDDocument document PDDocument.load(new File(contractFilePath))) { PDPage lastPage document.getPage(document.getNumberOfPages() - 1); PDRectangle mediaBox lastPage.getMediaBox(); float pageWidth mediaBox.getWidth(); float pageHeight mediaBox.getHeight(); PDImageXObject sealImage PDImageXObject.createFromFile(sealPngPath, document); float targetWidth 90f; // 印章显示宽度单位 pt float targetHeight targetWidth; // 正圆章宽高一致 float margin 50f; PDPageContentStream cs new PDPageContentStream( document, lastPage, AppendMode.APPEND, true, true); cs.drawImage(sealImage, pageWidth - targetWidth - margin, pageHeight - targetHeight - margin, targetWidth, targetHeight); cs.close(); document.save(signedFilePath); }逻辑说明PDDocument.load打开合同原文件getPage(getNumberOfPages() - 1)取最后一页getMediaBox()拿到页面尺寸PDImageXObject.createFromFile把 PNG 加载成 PDFBox 可用的图像对象drawImage的四个参数分别是 x、y、宽、高。坐标公式pageWidth - targetWidth - margin的意思是想让图片右边和上边距页面边缘各留 50pt就从页面宽减去印章宽再减去边距。参数说明targetWidth90f大约是 3.2 厘米印出来接近真实印章大小AppendMode.APPEND表示在已有内容之上叠加不会覆盖页面的原始内容流。如果要把章盖到落款处公司名称下方坐标就换一种算法目标位置x 坐标公式y 坐标公式右上角pageWidth - sealWidth - marginpageHeight - sealHeight - margin落款处居中(pageWidth - sealWidth) / 2pageHeight * 0.25 - sealHeight / 2签发日期下方(pageWidth - sealWidth) / 2 offsetpageHeight * 0.18 - sealHeight4.3 批量给多份合同盖章逐份 save不要全部读进内存实际业务里不会只有一份合同合同管理系统的盖章接口通常要批量处理几十上百份 PDF。常见错误做法是先把所有文件都PDDocument.load到内存处理完再统一保存合同数量一多直接 OOM。正确做法是循环里逐份打开、逐份保存、及时释放。每份合同文件单独处理互不干扰失败了也能单独重试。for (File contractFile : contractDir.listFiles(f - f.getName().endsWith(.pdf))) { try (PDDocument doc PDDocument.load(contractFile)) { PDPage sealPage doc.getPage(doc.getNumberOfPages() - 1); drawSealOnPage(doc, sealPage, sealImage, position); File output new File(outDir, contractFile.getName() _signed.pdf); doc.save(output); } }逻辑说明listFiles只处理 PDF 后缀的文件try (PDDocument)保证每个文件处理完流自动关闭drawSealOnPage是前面贴章逻辑的封装_signed.pdf是输出文件名后缀。这里有一个容易忽略的点PDDocument.load读取的是整个 PDF 文件的解析结果不是内存映射所以循环处理时如果内存不够可以考虑分批次处理每批最多 20 份处理完一批再进入下一批。我一般在真实接口里还会加一个线程池把文件路径列表拆成多个任务并行处理但要注意 PDFBox 的PDDocument不保证线程安全每个线程必须持有自己独立实例。5. 签章生成常见问题排查乱码、黑底、坐标漂移与尺寸失真的五个重灾区这部分写的都是我实际踩过的坑。电子签章看起来简单但真正跑起来翻车点比想象中多而且很多问题不是看报错就能发现的——程序没有异常运行也正常可生成的图片就是不对。下面五条是签章生成和 PDF 合成最常出现的疑难杂症每一条都按现象、原因、解决的顺序说透。5.1 生成阶段黑底、乱码、五角星翻转第一坑背景变成纯黑或纯白透明通道丢失。现象是签章 PNG 放到合同 PDF 上原本该透明的地方是一整块色块把合同文字都盖住了。原因主要有两个一是画布创建时用了TYPE_INT_RGB这个类型不含 Alpha 通道二是保存文件时格式写成了jpgJPEG 根本无法编码透明信息。解决方法在 2.1 节已经讲过画布强制用TYPE_INT_ARGB保存强制用png。另外如果代码里做过BufferedImage类型转换一定要在生成器出口调用一次getColorModel().hasAlpha()做断言。第二坑印章上的汉字变成乱码或方块。现象是公司名称在本地开发环境显示正常部署到 Linux 服务器后文字全部变成方框「口口口」。原因几乎可以断定是服务器上没有安装中文字体。Java 的Graphics2D绘制文字时依赖系统字体Font(仿宋)在 Windows 上能找到在精简版 Linux 容器里就找不到找不到就 fallback 到系统默认字体而默认字体不含中文时就会输出方块。解决的常见做法有两种第一种是部署时在系统里安装中文字体包第二种更稳妥——直接把字体文件打进 jar 包运行时动态加载不依赖系统环境InputStream fontStream SealGenerator.class.getResourceAsStream(/fonts/fangsong.ttf); Font font Font.createFont(Font.TRUETYPE_FONT, fontStream) .deriveFont(Font.PLAIN, fontSize); g2d.setFont(font);这段代码解决乱码的思路是把仿宋字体文件放在资源目录启动时通过Font.createFont从输入流创建字体对象然后deriveFont设置字号。这样无论在哪台机器上运行字体都来自资源包不再查询系统字体表效果完全一致。注意两点Font.createFont必须在绘制方法里调用一次即可不要每次画字都加载字体文件否则性能扛不住加载后的 Font 对象要先registerFont注册到图形环境否则部分 JDK 版本绘制时识别不了。第三坑五角星上下颠倒或角度不对。现象是五角星的一个尖角指向正下方而不是正上方或者整个星形像被旋转了 36 度。原因在 2.3 节有提示——数学坐标系的 y 轴向上而屏幕坐标系的 y 轴向下。解决方式是绘制时统一用y centerY - Math.sin(angle) * r也就是手工做一次 Y 轴翻转。不要在代码里用g2d.getTransform().scale(1, -1)去整体翻转因为翻转会影响后面文字的绘制方向容易把圆弧文字变镜像字。5.2 合成阶段坐标偏差、尺寸失真第四坑印章位置和预期不一致有的甚至落在页面外。现象是同样的坐标公式在 A4 竖版合同上正常换成横向合同或自定义页面尺寸就跑到页面外。原因很直接PDF 页面尺寸不是恒定不变的横向合同的MediaBox宽高刚好互换。解决方式是用getMediaBox()动态获取宽高再基于这个宽高做相对坐标计算不要写死 595 和 842 这两个数字。第五坑印章打印出来尺寸不对或者屏幕上看着正常、一打印就变得很大或很小。现象是 600x600 像素的印章 PNG贴到 PDF 上后实际打印出 20 多厘米宽。原因是没有做从像素到 PDF 单位pt的换算。PDF 的单位是 pt1 英寸 72pt而 PNG 图片的像素没有物理尺寸概念drawImage时只认 pt。如果直接把 600 作为targetWidth等于把 600 个像素当成 600 个 pt 用600pt 约 21 厘米当然尺寸离谱。解决方式是把像素根据 DPI 换算成 pttargetWidth 600 / 300 * 72也就是 144pt。通常合同章显示宽度 70pt 到 100pt 之间比较合适也就是实际打印直径 2.5 到 3.5 厘米。这些坑在本地自测时往往都不明显一旦换环境、换数据、换打印场景就集中爆发。我每次排查这类问题都有一套固定套路先确认生成的 PNG 目录下用图片查看器看透明背景是否保留再用 PDF 阅读器查看叠加位置和大小最后才去翻代码。多数问题其实卡在前两步。6. 进阶验证用像素检测与指纹校验确保证章效果可交付把签章盖到 PDF 上只是第一步更关键的是怎么验证这枚章真的盖对了。我后面在一个项目里发现印章 PNG 在视觉上没有异常但红色像素占比异常低一查是绘制比例的问题星形和文字占的区域太小整体像一枚偷工减料的假章。这让我养成了一个习惯生成完印章后先做一次像素级校验再做一次文件指纹校验两道检查都通过才允许进入业务库。像素级校验的思路很简单遍历生成后的BufferedImage统计不透明且红色通道占主导的像素占比占比应落在正常区间。如果占比异常低说明绘制的图形面积有问题如果占比异常高说明红色背景没有透明化。下面是一段可复用的检测代码int opaqueRed 0; int total 0; for (int x 0; x image.getWidth(); x) { for (int y 0; y image.getHeight(); y) { Color c new Color(image.getRGB(x, y), true); if (c.getAlpha() 200 c.getRed() 140 c.getGreen() 100) { opaqueRed; } total; } } double ratio opaqueRed * 1.0 / total; System.out.println(红色像素占比 String.format(%.2f, ratio));逻辑说明new Color(rgb, true)的第二个参数表示读取 Alpha 通道然后同时判断三个条件——不透明度大于 200、红色通道大于 140、绿色通道小于 100这三个条件同时满足才认为是印章的有效红色像素。参数说明ratio的正常范围一般是 0.08 到 0.25尺寸越大红章在整个画布中的占比会略微下降如果低于 0.05 就要检查绘制半径是否配置得过大如果高于 0.5多半是有背景色没抠掉。指纹校验的目的是防止合同文件被篡改。生成签章后把印章 PNG 的二进制数据和合同 PDF 的二进制数据分别做 SHA-256 摘要然后落库。之后每次调取合同原文时重新计算哈希值和库里存的值对不上说明文件被动过手脚这时应触发合规告警。哈希计算代码很简单MessageDigest digest MessageDigest.getInstance(SHA-256); try (InputStream in new FileInputStream(contractFile)) { byte[] hash digest.digest(in.readAllBytes()); String hex HexFormat.of().formatHex(hash); System.out.println(合同文件指纹 hex); }逻辑说明MessageDigest是 JDK 自带的摘要工具digest方法接收文件全部字节返回 32 字节的哈希值再用HexFormat转成十六进制字符串便于落库。参数说明指纹落库和 PDF 文件本身放同一个数据库表以及加一列系统名称方便以后对接多个合同的签章记录。从那以后我每次写完签章模块都强制走一遍透明背景检测和坐标命中检测然后再跑哈希入库合同系统上线到现在没有出过盖章事故。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑