跳到主要内容

电子合同制作教程

· 阅读需 11 分钟
ahKevinXy
作者

本文用 Go 将 HTML 合同模板批量生成 PDF,并介绍印章、签名图片和骑缝章的排版方法。图片章和签名图只负责呈现外观;正式电子签署还需要身份核验、签署意愿记录及 PDF 数字签名。

提醒:PDF 文件本身不等于可靠的电子签名。涉及正式签约时,应使用符合适用法律和业务要求的电子签约平台,并妥善保存身份验证、签署意愿、签署时间及文件完整性等记录。以下示例仅用于文件制作。 更新日期:2026-10-11。

一、用 HTML 排版并生成 PDF​

HTML 适合合同内容需要重复生成、统一样式或从系统数据填充的场景。先用 HTML 和 CSS 编写合同,再通过浏览器或程序打印为 PDF。

编写合同 HTML​

新建 contract.html,示例:

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
<style>
@page { size: A4; margin: 22mm 20mm; }
body {
color: #222;
font-family: "Noto Serif CJK SC", "Microsoft YaHei", serif;
font-size: 12pt;
line-height: 1.8;
}
h1 { text-align: center; font-size: 20pt; margin: 0 0 24pt; }
h2 { font-size: 14pt; margin: 18pt 0 6pt; }
.parties { margin-bottom: 20pt; }
.signature { margin-top: 36pt; page-break-inside: avoid; }
.seal { width: 120px; height: 120px; vertical-align: middle; }
.signature-image { max-width: 180px; max-height: 64px; vertical-align: middle; }
.keep-together { page-break-inside: avoid; }
</style>
</head>
<body>
<h1>{{.Title}}</h1>
<div class="parties">
<p>甲方:{{.PartyA}}</p>
<p>乙方:{{.PartyB}}</p>
<p>签订日期:{{.Date}}</p>
</div>

<h2>第一条 服务内容</h2>
<p>双方约定的服务内容、交付标准及期限如下:____________________。</p>

<h2>第二条 费用与支付</h2>
<p>合同费用及支付安排:____________________。</p>

<h2>第三条 其他约定</h2>
<p>____________________。</p>

<div class="signature">
<p>甲方演示印章:</p>
<svg class="seal" viewBox="0 0 160 160" role="img" aria-label="演示用印章,非有效印章">
<circle cx="80" cy="80" r="73" fill="none" stroke="#c62828" stroke-width="5"/>
<circle cx="80" cy="80" r="66" fill="none" stroke="#c62828" stroke-width="2"/>
<text x="80" y="56" text-anchor="middle" fill="#c62828" font-size="13">{{.PartyA}}</text>
<text x="80" y="91" text-anchor="middle" fill="#c62828" font-size="25" font-weight="bold">演示</text>
<text x="80" y="116" text-anchor="middle" fill="#c62828" font-size="12">非有效印章</text>
</svg>
<p>乙方签名:<img class="signature-image" src="{{.SignatureDataURI}}" alt="签名图片"> 日期:{{.Date}}</p>
</div>
</body>
</html>

根据实际业务补充条款,并确认合同中的主体名称、金额、日期和附件等信息准确无误。打印样式可通过 @page 设置纸张和页边距;签署栏可使用 page-break-inside: avoid,减少被分页拆开的情况。

印章与签名素材​

上面的 SVG 是直接写在 HTML 模板中的演示章:两个 circle 画出章边,text 放置单位名称和演示标记,Go 的 html/template 再将 {{.PartyA}} 填入名称。SVG 是矢量格式,缩放打印不易模糊。正式业务不要用代码仿造真实公章,应由印章管理系统或电子签约平台返回已授权的章图及对应记录,再由模板展示。

签名图片应由签署人在完成身份验证和明确授权后主动绘制或上传;应用可将画布导出为 PNG,由 Go 读取并编码成 SignatureDataURI 嵌入模板。不要把姓名套入手写字体后当作本人签名。生产系统还应把签署人、合同版本或摘要、操作时间和同意动作关联保存。图片只呈现外观,不能证明签名者身份、签署意愿或文件未被修改。

导出 HTML 为 PDF​

使用浏览器导出​

在 Chrome 或 Edge 中打开 contract.html,选择“打印”,将目标打印机设为“另存为 PDF”,检查纸张、边距、页眉页脚和背景图形后保存。导出后重新打开 PDF,检查分页、字体、表格和签署栏。

使用 Go 批量生成​

使用 Go 的 html/template 填充合同字段,再通过 chromedp 调用本机 Chrome 或 Chromium 打印 PDF:

go mod init contract-pdf
go get github.com/chromedp/chromedp

电脑或服务器还需要安装 Chrome 或 Chromium。将下面代码保存为 main.go,与 contract.html 放在同一目录。示例为每份合同分别指定签名图片:

package main

import (
"context"
"encoding/base64"
"fmt"
"html/template"
"log"
"net/url"
"os"
"path/filepath"

"github.com/chromedp/chromedp"
"github.com/chromedp/cdproto/page"
)

type Contract struct {
Title string
PartyA string
PartyB string
Date string
SignaturePNG string
}

type TemplateData struct {
Title string
PartyA string
PartyB string
Date string
SignatureDataURI string
}

func pngDataURI(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(data), nil
}

func main() {
contracts := []Contract{
{Title: "服务合同", PartyA: "示例甲方有限公司", PartyB: "示例乙方有限公司", Date: "2026-10-11", SignaturePNG: "signatures/party-b.png"},
{Title: "采购合同", PartyA: "示例丙方有限公司", PartyB: "示例丁方有限公司", Date: "2026-10-12", SignaturePNG: "signatures/party-d.png"},
}

contractTemplate, err := template.ParseFiles("contract.html")
if err != nil {
log.Fatal(err)
}
if err := os.MkdirAll("output", 0755); err != nil {
log.Fatal(err)
}

ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()

for index, contract := range contracts {
signatureDataURI, err := pngDataURI(contract.SignaturePNG)
if err != nil {
log.Fatal(err)
}
data := TemplateData{
Title: contract.Title,
PartyA: contract.PartyA,
PartyB: contract.PartyB,
Date: contract.Date,
SignatureDataURI: signatureDataURI,
}

htmlPath := filepath.Join("output", fmt.Sprintf("contract-%02d.html", index+1))
htmlFile, err := os.Create(htmlPath)
if err != nil {
log.Fatal(err)
}
err = contractTemplate.Execute(htmlFile, data)
closeErr := htmlFile.Close()
if err != nil {
log.Fatal(err)
}
if closeErr != nil {
log.Fatal(closeErr)
}

absoluteHTMLPath, err := filepath.Abs(htmlPath)
if err != nil {
log.Fatal(err)
}
fileURL := (&url.URL{Scheme: "file", Path: filepath.ToSlash(absoluteHTMLPath)}).String()

var pdfData []byte
if err := chromedp.Run(ctx,
chromedp.Navigate(fileURL),
chromedp.ActionFunc(func(ctx context.Context) error {
var err error
pdfData, _, err = page.PrintToPDF().WithPrintBackground(true).Do(ctx)
return err
}),
); err != nil {
log.Fatal(err)
}

pdfPath := filepath.Join("output", fmt.Sprintf("contract-%02d.pdf", index+1))
if err := os.WriteFile(pdfPath, pdfData, 0644); err != nil {
log.Fatal(err)
}
fmt.Println("已生成", pdfPath)
}
}

运行 go run main.go 后,output 目录中会为每份合同生成一个 HTML 和 PDF。部署环境需安装中文字体并允许 Chromium 启动。正式签署时,所有内容和印章图片应先确定,再由电子签约服务完成身份核验、签署记录和 PDF 数字签名。

制作骑缝章效果​

骑缝章需要在 PDF 页数和分页固定后添加,否则正文改动导致分页变化时,章片会错位。下面以两页为例:将一张透明背景的演示印章 PNG 按中线切成左右两片,再把左右片叠加到相邻 PDF 页的相同位置。打印后把两页对齐,视觉上可以拼回整章。

将下面代码保存为 seal_split.go。它不绘制或仿造印章,只负责裁切你有权使用的 PNG 图片,并保留透明通道和两侧尺寸:

package main

import (
"fmt"
"image"
"image/draw"
"image/png"
"os"
)

func splitSeal(input, leftPath, rightPath string) error {
file, err := os.Open(input)
if err != nil {
return err
}
defer file.Close()

seal, err := png.Decode(file)
if err != nil {
return err
}
bounds := seal.Bounds()
if bounds.Dx()%2 != 0 {
return fmt.Errorf("印章图片宽度必须是偶数像素")
}

middle := bounds.Min.X + bounds.Dx()/2
partBounds := image.Rect(0, 0, bounds.Dx()/2, bounds.Dy())
left := image.NewRGBA(partBounds)
right := image.NewRGBA(partBounds)
draw.Draw(left, partBounds, seal, bounds.Min, draw.Src)
draw.Draw(right, partBounds, seal, image.Pt(middle, bounds.Min.Y), draw.Src)

for path, part := range map[string]*image.RGBA{leftPath: left, rightPath: right} {
output, err := os.Create(path)
if err != nil {
return err
}
encodeErr := png.Encode(output, part)
closeErr := output.Close()
if encodeErr != nil {
return encodeErr
}
if closeErr != nil {
return closeErr
}
}
return nil
}

生成章片时调用 splitSeal("demo-seal.png", "seal-left.png", "seal-right.png")。demo-seal.png 应是透明背景的授权素材;教程中的演示章可以用于测试排版,但不能冒充真实印章。

安装 PDF 叠章库:

go get github.com/pdfcpu/pdfcpu

在生成最终 PDF 后调用 pdfcpu。本例把左半章放在第 1 页、右半章放在第 2 页;pos 和 scale 需要按纸张尺寸及印章素材调整,并用测试文件检查两页坐标是否一致:

package main

import (
"context"

"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
)

func addCrossPageSeal(inputPDF, outputPDF string) error {
ctx := context.Background()
conf := model.NewDefaultConfiguration()
position := "pos:r, scale:0.3"
intermediatePDF := "output/with-page-1-seal.pdf"

if err := api.AddImageWatermarksFile(
ctx, inputPDF, intermediatePDF, []string{"1"}, true,
"seal-left.png", position, conf,
); err != nil {
return err
}
return api.AddImageWatermarksFile(
ctx, intermediatePDF, outputPDF, []string{"2"}, true,
"seal-right.png", position, conf,
)
}

示例只演示一对相邻页面。纸质骑缝章通常是在全部页面打印、排序并对齐后,使用真实授权印章在页边连续加盖;PDF 上的拆分图片只是视觉标识,不能防止页面替换或内容修改。电子文件应在所有页面处理完毕后,对最终 PDF 做数字签名,或交由电子签约平台签署。

二、将图片合成为 PDF​

这种方法适用于纸质合同扫描件、逐页导出的图片,或已经制作好的合同页面图片。尽量使用清晰、方向正确的原图;图片过小会导致 PDF 放大后模糊。

准备图片​

将页面图片按顺序命名,例如:

pages/
001.jpg
002.jpg
003.png

建议统一页面尺寸和方向。扫描文件时可选择 300 DPI 左右的分辨率;黑白文字文件可使用灰度或黑白模式,以控制文件大小。

用 Python 和 Pillow 合成​

安装 Pillow:

python -m pip install pillow

以下脚本会按文件名排序,将 JPG、JPEG、PNG 图片逐张放入多页 PDF:

from pathlib import Path
from PIL import Image, ImageOps

files = sorted(
path for path in Path("pages").iterdir()
if path.suffix.lower() in {".jpg", ".jpeg", ".png"}
)

if not files:
raise SystemExit("pages 目录中没有可处理的图片")

pages = []
for path in files:
with Image.open(path) as source:
image = ImageOps.exif_transpose(source).convert("RGB")
pages.append(image.copy())

first, *rest = pages
first.save(
"contract-from-images.pdf",
"PDF",
save_all=True,
append_images=rest,
resolution=150.0,
)

for image in pages:
image.close()

图片的像素尺寸和 PDF 页面物理尺寸并非总能自动匹配。对于需要固定 A4 页面、统一缩放或设置留白的文件,可使用 ReportLab 将图片绘制到指定页面大小:

python -m pip install reportlab pillow
from pathlib import Path
from PIL import Image, ImageOps
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas

files = sorted(
path for path in Path("pages").iterdir()
if path.suffix.lower() in {".jpg", ".jpeg", ".png"}
)
if not files:
raise SystemExit("pages 目录中没有可处理的图片")

output = canvas.Canvas("contract-a4.pdf", pagesize=A4)
page_w, page_h = A4
margin = 24 # PDF 点数,约为 8.5 毫米

for path in files:
with Image.open(path) as source:
image = ImageOps.exif_transpose(source).convert("RGB")
image_w, image_h = image.size
scale = min(
(page_w - 2 * margin) / image_w,
(page_h - 2 * margin) / image_h,
)
draw_w, draw_h = image_w * scale, image_h * scale
x, y = (page_w - draw_w) / 2, (page_h - draw_h) / 2
output.drawImage(
image, x, y, width=draw_w, height=draw_h,
preserveAspectRatio=True,
)
output.showPage()

output.save()

此示例会保持图片比例并居中放入 A4 页面,不会拉伸图片;图片比例与纸张不同的部分会留下空白。

三、导出后的检查​

  • 核对合同双方、金额、日期、条款和附件是否完整。
  • 检查 PDF 页数、顺序、方向、分页、边距和中文字体显示。
  • 确认扫描图片清晰可读,签章或手写内容没有被裁切。
  • 使用可靠方式保存最终文件,并避免未经授权地修改签署后的文件。
  • 如需双方在线签署,使用适合该业务的电子签约流程,而不是仅把签名图片贴到 PDF 上。

总结​

批量制作合同可由 Go 填充 HTML 模板并调用 Chromium 输出 PDF。演示印章可用 SVG 绘制;正式印章和手写签名应来自授权素材或电子签约平台,不能靠字体仿造。骑缝效果是在 PDF 分页固定后,将印章图切片并叠加到指定页面。以上图片都只是视觉元素;需要验证身份和文件完整性时,应在内容最终确定后通过电子签约平台完成 PDF 数字签名,签名后不要再修改文件。