pdf-sign-kit
English
pdf-sign-kit is a framework-agnostic Python toolkit for stamping signatures, seals and machine text onto PDF documents. The library — including its API design, geometry-detection algorithms, watermark/seam logic and workflow rule engine — was independently designed and written from scratch by the author; no part of it was extracted from, referenced from, or based on any existing project. It works directly with local files: no web framework, no database, and no network connection are required.
Feature map
| Capability | Module | Key API |
|---|---|---|
| Inspect pages (count / size / raster preview) | preview |
page_count, page_size, render_page, render_page_png |
| Detect table cells from borders | geometry |
find_table_cell, TableCell |
| Stamp images onto pages | placement |
stamp, stamp_batch, StampResult |
| Cross-platform font discovery | fonts |
find_cjk_font, load_cjk_font, … |
| Raster text at 600 DPI, wrap support | text |
render_text_image, render_wrapped_text_image, stamp_text |
| Built-in "同意" / check-mark stamps | stamps |
make_agree_image, make_checkmark_image, stamp_agree, stamp_checkmark |
| Make near-white backgrounds transparent | background |
remove_background |
| Bitmap concatenation / scaling | compose |
hconcat, vconcat, scale_to_height, scale_to_width |
| Locate stamps by visible text | anchor |
find_text, find_all_text, stamp_at_anchor |
| Diagonal watermarks | watermark |
render_watermark_overlay, stamp_watermark |
| Cross-page seam stamps (骑缝章) | seam |
stamp_seam |
| Configurable signing schemes | scheme |
FlowScheme, LevelSpec, Initiator, build_metadata |
| Signing rule engine | workflow |
can_sign, can_sign_level, all_completed, all_signed_strict |
| Identity normalization | identity |
bare_id, identity_matches |
| Signature log tables | logtable |
render_log_image, render_log_png_bytes, render_log_data_url |
| Chinese keyword extraction | keywords |
extract_keywords |
| Reusable placement templates | template |
PlacementTemplate, OpSpec, save_template, load_template |
| Exception hierarchy | exceptions |
PdfSignKitError, InvalidPdfError, … |
Installation
pip install pdf-sign-kit
Requirements (installed automatically): Python ≥ 3.9, PyMuPDF, Pillow, opencv-python-headless, NumPy.
Quickstart
import pdf_sign_kit as psk
# 1. Inspect the document
print(psk.page_count("form.pdf")) # e.g. 2
print(psk.page_size("form.pdf", page=0)) # (595.0, 842.0)
# 2. Stamp an existing signature image; the file is edited in place
result = psk.stamp("form.pdf", "signature.png", x=200, y=127)
print(result.center_x, result.center_y, result.page)
# 3. Built-in stamps
psk.stamp_agree("form.pdf", x=200, y=227)
psk.stamp_checkmark("form.pdf", x=200, y=327)
# 4. Mixed Chinese/Latin machine text
psk.stamp_text("form.pdf", "编号 BZ-001", x=100, y=700, font_size=12)
# 5. Clean a scan, render a preview, compute an integrity hash
transparent = psk.remove_background("scan.png", threshold=235)
transparent.save("stamp.png")
preview = psk.render_page("form.pdf", page=0, zoom=3)
digest = psk.hash_file("form.pdf") # SHA-256 hex string
Core concepts
Coordinate system. All public stamping functions take x / y in PDF
points (1 pt = 1/72 inch) measured from the page's bottom-left corner
in the orientation the page is displayed in. Page indices are zero-based.
Page rotation is handled internally. On pages rotated by 90/180/270 degrees you still pass display-orientation coordinates; the library maps them through the page's de-rotation matrix automatically.
Participant metadata. The workflow engine consumes a plain list of dict
records. Fields produced by build_metadata:
| Field | Meaning |
|---|---|
person |
signer identifier (ID string) |
name |
display name |
department |
department/display text |
role |
initiator / signer |
state |
status code (see below) |
sign_time, upload_time |
ISO timestamps |
sign_order, level |
ordering / signing level |
sign_type |
personal / group / department |
is_proxy |
proxy signature flag |
sign_group |
present on GROUP levels (personal countersign) |
department_sign_group |
present on DEPARTMENT levels |
rejected_reason |
reason text after rejection |
State codes. "0" pending · "1" signed · "2" completed by another
member of the same group · "3" rejected.
Stamp modes. GENERAL ordinary signature · HANDWRITTEN loose
handwriting fit · TEXT rasterized text · AGREE fixed "同意" stamp ·
CHECKMARK check mark.
Module guide
1. preview — inspect and render pages
psk.page_count("form.pdf") # -> int
psk.page_size("form.pdf", page=0) # -> (width_pt, height_pt)
img = psk.render_page("form.pdf", page=0, zoom=3, alpha=False) # PIL image
png = psk.render_page_png("form.pdf", page=0, zoom=3) # bytes
zoom scales relative to 72 DPI (3 → 216 DPI). alpha=True renders RGBA.
2. geometry — automatic table-cell detection
cell = psk.find_table_cell("form.pdf", page=0, x=200, y=127, dpi=300)
# TableCell(left, top, width, height, raw_left) or None
The page is rasterized once as grayscale; only the row/column crossing the
click is analyzed (1-D Otsu). raw_left is the exact detected border;
left carries a small inward offset. Searches that would exceed
max_width / max_height (defaults 2000 / 40 pt) return None.
3. placement — the stamping core
result = psk.stamp(
"form.pdf", "signature.png",
x=200, y=127,
page=0,
out_path=None, # None -> edit in place via a temp file + atomic commit
width=None, height=None,
mode=psk.GENERAL,
detect_cell=True, # False -> place at natural size, skip cell detection
)
# StampResult(center_x, center_y, page, pdf_path)
Batch many stamps in a single open/save cycle — one failed operation yields
None at its position without aborting the rest:
results = psk.stamp_batch(
"form.pdf",
[
{"image": "signature.png", "x": 200, "y": 127, "page": 0},
{"image": "date.png", "x": 200, "y": 227, "page": 0,
"mode": psk.TEXT, "detect_cell": False},
],
)
Operation keys: image, x, y, optional page, mode, width,
height, logical_size, cell, detect_cell. image accepts a path,
PIL image, or bytes.
4. fonts — cross-platform font discovery
psk.find_cjk_font() # serif-first path (SimSun / Noto Serif), or None
psk.find_ui_cjk_font() # sans-serif-first (Microsoft YaHei / PingFang / Noto Sans)
psk.find_latin_font() # Arial / Times / Liberation / DejaVu
font = psk.load_cjk_font(24) # cached FreeTypeFont
font = psk.load_ui_cjk_font(24)
Explicit paths are honored first. No font is bundled. resolve_cjk_font()
raises FontNotFoundError (with install guidance) when nothing usable
exists.
5. text — high-resolution raster text (600 DPI)
single = psk.render_text_image(
"编号 BZ-001", font_size=12, color=(0, 0, 128),
)
# TextImage(image, width, height) — logical size in PDF points
wrapped = psk.render_wrapped_text_image(
"本文件仅用于……", font_size=12, max_width=200, line_spacing=1.3,
)
psk.stamp_text("form.pdf", "编号 BZ-001", x=100, y=700)
Chinese/Latin runs are split and drawn with their respective fonts.
horizontal_scale stretches the image horizontally when provided.
6. stamps — built-in images and convenience stamps
mark = psk.make_checkmark_image(size=30, color=(0, 0, 0, 255), stroke=1)
agree = psk.make_agree_image("同意", font_size=14)
psk.stamp_checkmark("form.pdf", x=200, y=327)
psk.stamp_agree("form.pdf", x=200, y=227)
7. background — transparent stamps from scans
img = psk.remove_background("scan.png", threshold=235) # -> RGBA image
Pixels whose RGB channels are all ≥ threshold become fully transparent.
Accepts a path, PIL image, or bytes.
8. compose — bitmap assembly
strip = psk.hconcat([name_img, date_img], gap=8, align="middle")
stack = psk.vconcat([title_img, body_img], gap=4, align="center")
bigger = psk.scale_to_height(small_img, target_height=60)
wider = psk.scale_to_width(small_img, target_width=200)
Cross-axis alignment: hconcat top/middle/bottom; vconcat
left/center/right. Inputs are never modified.
9. anchor — text-anchor placement (borderless documents)
anchor = psk.find_text("contract.pdf", "签字:") # first match
anchors = psk.find_all_text("contract.pdf", "签字:") # every match
# restrict search: pages=[0,1]; pick occurrence=2
psk.stamp_at_anchor(
"contract.pdf", "signature.png", anchor,
position=psk.AFTER, # AFTER / BELOW / OVER the matched text
gap=3,
)
The anchor exposes left/top/width/height and point(page_height, position, gap) for custom geometry; to_cell() returns a TableCell.
10. watermark — diagonal watermarks
overlay = psk.render_watermark_overlay(
595, 842, "作废", font_size=72, angle=45,
color=(220, 38, 38, 110), tile=False,
)
psk.stamp_watermark(
"contract.pdf", "作废", pages=[0, 1],
color=(220, 38, 38, 90), tile=True,
)
color is RGBA — alpha directly controls opacity. Tiling repeats the
caption over the whole page instead of one centered mark.
11. seam — cross-page seam stamps
results = psk.stamp_seam(
"contract.pdf", "seal.png",
start_page=0,
page_count=None, # None -> all remaining pages
edge=psk.RIGHT, # or psk.LEFT
visible_width_pt=None, # default: 45% of seal width
y_center_pt=None, # default: page vertical center (top-origin)
)
The seal is sliced into one strip per page; strips sit partly outside the page boundary and reassemble when the pages are fanned. Rotated pages are supported.
12. scheme — configurable signing schemes
from pdf_sign_kit import PERSONAL, GROUP, DEPARTMENT
# Build a scheme directly
scheme = psk.FlowScheme(
levels=(
psk.LevelSpec(level=1, name="起草", mode=PERSONAL),
psk.LevelSpec(level=2, name="会签", mode=GROUP),
psk.LevelSpec(level=3, name="部门会签", mode=DEPARTMENT),
)
)
# Or use the factory: N contiguous levels with name/mode overrides
scheme = psk.FlowScheme.create(
3,
names={1: "起草", 2: "会签", 3: "部门会签"},
modes={2: GROUP, 3: DEPARTMENT},
)
psk.FlowScheme.default() # built-in 8-level scheme
PERSONAL— everyone assigned to the level must sign;GROUP— any one member signs and the level completes (asign_groupfield is generated);DEPARTMENT— each department needs one signer; all departments must finish (usesdepartment_sign_group).
LevelSpec and FlowScheme are immutable, validated, and round-trip
through to_dict() / from_dict().
The Initiator describes the document originator — either a level-0
marker only, or a participant who signs at a given level:
initiator = psk.Initiator(id="I", name="发起人", level=0, require_signature=False)
build_metadata(scheme, assignments, initiator=None) produces the
participant records; assignments map level → list of person descriptors
({"id", "name"}; DEPARTMENT people also carry group / group_name).
Pass now=<callable> for deterministic timestamps.
13. workflow — signing rule engine
can, message = psk.workflow.can_sign(metadata, "A")
can, message = psk.workflow.can_sign_level(metadata, "A", target_level=2)
done, message = psk.workflow.all_completed(metadata)
strict = psk.workflow.all_signed_strict(metadata)
The engine enforces level ordering, in-level sign_order, group/department
gating, and blocks any document containing a "3" rejected record.
14. identity — identifier normalization
psk.bare_id("123 - Zhang") # -> "123"
psk.identity_matches("00123", "123") # -> True (numeric, leading zeros)
15. logtable — signature log tables
sig_logs = [
{"工号": "A", "姓名": "甲", "等级": "Lv1", "签字时间": "2026-09-28T10:00"},
]
rejection_logs = []
image = psk.render_log_image("contract.pdf", sig_logs, rejection_logs)
buffer = psk.render_log_png_bytes("contract.pdf", sig_logs, rejection_logs)
url = psk.render_log_data_url("contract.pdf", sig_logs, rejection_logs)
# "data:image/png;base64,...."
State colors: signed = green, pending = amber, rejected = red.
16. keywords — Chinese keyword extraction
psk.extract_keywords("图纸标注不清晰,请核对") # -> ["图纸", "标注", "清晰", "核对"]
Extracts 2–4 character CJK runs, filters a built-in stop-word set, and falls back to a truncated slice of the input when nothing matches.
17. template — reusable placement templates
template = psk.PlacementTemplate.capture(
[{"image": "signature.png", "x": 200, "y": 127, "page": 0}],
name="Form-A",
)
psk.save_template("form_a.json", template)
template = psk.load_template("form_a.json")
operations = template.instantiate({"sign": "new_signature.png"})
psk.stamp_batch("another_file.pdf", operations)
Templates store logical ref names instead of concrete image paths;
refs lists the required names, and instantiate() accepts an optional
page_offset.
18. exceptions — error hierarchy
PdfSignKitError is the base. Subclasses: InvalidPdfError,
InvalidImageError, PageOutOfRangeError, FontNotFoundError,
PlacementError.
End-to-end: a complete signing workflow
The following script is fully self-contained and runnable:
import os
import tempfile
from datetime import datetime
import fitz
from PIL import Image, ImageDraw
import pdf_sign_kit as psk
from pdf_sign_kit import FlowScheme, Initiator, LevelSpec, PERSONAL, GROUP
work = tempfile.mkdtemp(prefix="capstone_")
pdf_path = os.path.join(work, "contract.pdf")
# Step 1 — create a two-page PDF with two bordered signing cells
doc = fitz.open()
page = doc.new_page(width=595, height=842)
page.draw_rect(fitz.Rect(100, 700, 300, 730))
page.draw_rect(fitz.Rect(100, 600, 300, 630))
page.insert_text((100, 690), "Lv1 Sign:", fontsize=10)
page.insert_text((100, 590), "Lv2 Sign:", fontsize=10)
doc.new_page(width=595, height=842)
doc.save(pdf_path)
doc.close()
# Step 2 — draw a signature, remove its white background
sig = Image.new("RGB", (120, 50), (255, 255, 255))
ImageDraw.Draw(sig).line(
[(10, 40), (40, 10), (70, 35), (110, 15)], fill=(0, 0, 0), width=3
)
sig_path = os.path.join(work, "sign.png")
psk.remove_background(sig, threshold=235).save(sig_path)
# Step 3 — define a two-level scheme: personal draft, then countersign
scheme = FlowScheme(
levels=(
LevelSpec(level=1, name="起草", mode=PERSONAL),
LevelSpec(level=2, name="会签", mode=GROUP),
)
)
metadata = build_metadata(
scheme,
{
1: [{"id": "A", "name": "甲"}],
2: [{"id": "B", "name": "乙"}, {"id": "C", "name": "丙"}],
},
initiator=Initiator(id="I", name="发起人"),
)
# Click points at each cell's center (bottom-left origin)
clicks = {1: (200.0, 842 - 715.0), 2: (200.0, 842 - 615.0)}
# Step 4 — sign each level: ask the engine, stamp, update the records
for level in (1, 2):
pending = [r for r in metadata if r["level"] == level and r["state"] == "0"]
signer = pending[0]
can, message = psk.workflow.can_sign(metadata, signer["person"])
assert can, message
x, y = clicks[level]
psk.stamp(pdf_path, sig_path, x=x, y=y, page=0)
signer["state"] = "1"
signer["sign_time"] = datetime.now().isoformat()
for other in pending[1:]: # remaining GROUP members
other["state"] = "2"
other["sign_group_completed_by"] = signer["person"]
done, message = psk.workflow.all_completed(metadata)
assert done
# Step 5 — seam stamp across both pages, watermark the first page
psk.stamp_seam(pdf_path, sig_path)
psk.stamp_watermark(pdf_path, "作废", pages=[0], color=(220, 38, 38, 80))
# Step 6 — render the signature log
sig_logs = [
{"工号": r["person"], "姓名": r["name"], "等级": f'Lv{r["level"]}',
"签字时间": r["sign_time"]}
for r in metadata if r["role"] == "signer" and r.get("sign_time")
]
log_url = psk.render_log_data_url("contract.pdf", sig_logs, [])
# Step 7 — integrity hash; save/reuse a placement template
digest = psk.hash_file(pdf_path)
assert psk.verify_file_hash(pdf_path, digest)
template = psk.PlacementTemplate.capture(
[{"image": sig_path, "x": 200.0, "y": 127.0, "page": 0}],
name="contract-form",
)
psk.save_template(os.path.join(work, "tpl.json"), template)
Troubleshooting / FAQ
FontNotFoundErroron a minimal Linux image — install a CJK font (apt-get install fonts-noto-cjk) or passcjk_font_path/font_pathexplicitly.find_table_cellreturnsNone— the point is not enclosed by detectable borders. Usefind_text(text anchor), or stamp withdetect_cell=Falseand explicitwidth/height.- My transparent stamp shows a white box on the page — run
remove_background()on the image before stamping. - The stamp lands at the wrong spot on a rotated page — coordinates must be in the page's display orientation; do not pre-rotate them yourself.
- GROUP level does not complete after one signature — the records
must carry
sign_group(usebuild_metadatawith aGROUPlevel); setting onlysign_type="group"is insufficient. PageOutOfRangeError— pages are zero-based; check againstpage_count()first.
Notes & limitations
- Stamps are visual ("electronic") signatures — raster images placed on the page — not PKI/certificate-based ("digital") signatures with legal non-repudiation guarantees.
- Raster text is flattened (not selectable text).
- No fonts are bundled; they are auto-detected on the host system.
License
MIT © 兰衣
中文文档
pdf-sign-kit 是一个与框架无关的 Python 工具包,用于在 PDF 文档上加盖 签名、印章与机打文字。本库——包括 API 设计、几何检测算法、水印/骑缝 逻辑与工作流规则引擎——由作者独立设计、从零编写完成,未提取、参照或 基于任何现有项目。库直接操作本地文件:不依赖 Web 框架、数据库或网络 连接。
功能对照表
| 能力 | 模块 | 主要 API |
|---|---|---|
| 页面检查(页数/尺寸/栅格预览) | preview |
page_count、page_size、render_page、render_page_png |
| 按边框检测表格单元格 | geometry |
find_table_cell、TableCell |
| 图片盖章 | placement |
stamp、stamp_batch、StampResult |
| 跨平台字体发现 | fonts |
find_cjk_font、load_cjk_font 等 |
| 600 DPI 机打文字、自动换行 | text |
render_text_image、render_wrapped_text_image、stamp_text |
| 内置「同意」/打勾章 | stamps |
make_agree_image、make_checkmark_image、stamp_agree、stamp_checkmark |
| 近白背景转透明 | background |
remove_background |
| 位图拼接/缩放 | compose |
hconcat、vconcat、scale_to_height、scale_to_width |
| 按可见文字定位 | anchor |
find_text、find_all_text、stamp_at_anchor |
| 斜向水印 | watermark |
render_watermark_overlay、stamp_watermark |
| 骑缝章 | seam |
stamp_seam |
| 可配置签署方案 | scheme |
FlowScheme、LevelSpec、Initiator、build_metadata |
| 签署规则引擎 | workflow |
can_sign、can_sign_level、all_completed、all_signed_strict |
| 身份归一化 | identity |
bare_id、identity_matches |
| 签字日志表 | logtable |
render_log_image、render_log_png_bytes、render_log_data_url |
| 中文关键词提取 | keywords |
extract_keywords |
| 可复用签署位置模板 | template |
PlacementTemplate、OpSpec、save_template、load_template |
| 异常体系 | exceptions |
PdfSignKitError、InvalidPdfError 等 |
安装
pip install pdf-sign-kit
依赖(自动安装):Python ≥ 3.9、PyMuPDF、Pillow、opencv-python-headless、 NumPy。
快速上手
import pdf_sign_kit as psk
# 1. 检查文件
print(psk.page_count("表单.pdf")) # 如 2
print(psk.page_size("表单.pdf", page=0)) # (595.0, 842.0)
# 2. 加盖已有签名图片,原地修改文件
result = psk.stamp("表单.pdf", "签名.png", x=200, y=127)
print(result.center_x, result.center_y, result.page)
# 3. 内置图章
psk.stamp_agree("表单.pdf", x=200, y=227)
psk.stamp_checkmark("表单.pdf", x=200, y=327)
# 4. 中英混排机打文字
psk.stamp_text("表单.pdf", "编号 BZ-001", x=100, y=700, font_size=12)
# 5. 扫描件透视、渲染预览、计算完整性哈希
transparent = psk.remove_background("扫描件.png", threshold=235)
transparent.save("图章.png")
preview = psk.render_page("表单.pdf", page=0, zoom=3)
digest = psk.hash_file("表单.pdf") # SHA-256 十六进制串
核心概念
坐标系。 所有盖章函数的 x / y 单位为 PDF 点(1 pt = 1/72
英寸),以页面显示方向的左下角为原点。页码从 0 开始。
页面旋转由库内部处理。 对旋转 90/180/270 度的页面,仍传显示方向 坐标,库会自动通过页面反旋转矩阵换算。
参与者 metadata。 工作流引擎使用一个由字典组成的普通列表。
build_metadata 生成的字段:
| 字段 | 含义 |
|---|---|
person |
签署人标识(ID 字符串) |
name |
显示姓名 |
department |
部门/显示文本 |
role |
initiator(发起人)/ signer(签署人) |
state |
状态码(见下) |
sign_time、upload_time |
ISO 时间戳 |
sign_order、level |
排序/签署等级 |
sign_type |
personal / group / department |
is_proxy |
是否代签 |
sign_group |
GROUP(个人会签)等级上存在 |
department_sign_group |
DEPARTMENT(部门会签)等级上存在 |
rejected_reason |
驳回后的理由文本 |
状态码。 "0" 待签署 · "1" 已签署 · "2" 由同组其他成员代签完成
· "3" 已驳回。
盖章模式。 GENERAL 普通签名 · HANDWRITTEN 手写宽松适配 · TEXT
机打文字 · AGREE 固定「同意」章 · CHECKMARK 打勾。
模块详解
1. preview —— 页面检查与渲染
psk.page_count("表单.pdf") # -> int
psk.page_size("表单.pdf", page=0) # -> (宽pt, 高pt)
img = psk.render_page("表单.pdf", page=0, zoom=3, alpha=False) # PIL 图像
png = psk.render_page_png("表单.pdf", page=0, zoom=3) # bytes
zoom 相对 72 DPI 缩放(3 即 216 DPI);alpha=True 输出 RGBA。
2. geometry —— 表格单元格自动检测
cell = psk.find_table_cell("表单.pdf", page=0, x=200, y=127, dpi=300)
# TableCell(left, top, width, height, raw_left),找不到为 None
页面只渲染一次灰度图,仅分析穿过点击点的一行一列(1D Otsu 自适应
阈值)。raw_left 是精确检测到的边框,left 带少量内缩。检测结果
超过 max_width / max_height(默认 2000 / 40 pt)时返回 None。
3. placement —— 盖章核心
result = psk.stamp(
"表单.pdf", "签名.png",
x=200, y=127,
page=0,
out_path=None, # None → 临时文件 + 原子提交,原地修改
width=None, height=None,
mode=psk.GENERAL,
detect_cell=True, # False → 按自然尺寸放置,跳过单元格检测
)
# StampResult(center_x, center_y, page, pdf_path)
批量盖章只需一次打开/保存;单个操作失败时该位返回 None,不影响
其他操作:
results = psk.stamp_batch(
"表单.pdf",
[
{"image": "签名.png", "x": 200, "y": 127, "page": 0},
{"image": "日期.png", "x": 200, "y": 227, "page": 0,
"mode": psk.TEXT, "detect_cell": False},
],
)
操作字典支持的键:image、x、y,以及可选的 page、mode、
width、height、logical_size、cell、detect_cell。image
接受路径、PIL 图像或字节。
4. fonts —— 跨平台字体发现
psk.find_cjk_font() # 衬线优先(宋体 / Noto Serif),找不到为 None
psk.find_ui_cjk_font() # 无衬线优先(雅黑 / PingFang / Noto Sans)
psk.find_latin_font() # Arial / Times / Liberation / DejaVu
font = psk.load_cjk_font(24) # 带缓存的 FreeTypeFont
font = psk.load_ui_cjk_font(24)
显式路径优先;包不捆绑字体。无可用字体时 resolve_cjk_font() 抛出
FontNotFoundError(附安装指引)。
5. text —— 高清机打文字(600 DPI)
single = psk.render_text_image(
"编号 BZ-001", font_size=12, color=(0, 0, 128),
)
# TextImage(image, width, height)——逻辑尺寸以 PDF 点计
wrapped = psk.render_wrapped_text_image(
"本文件仅用于……", font_size=12, max_width=200, line_spacing=1.3,
)
psk.stamp_text("表单.pdf", "编号 BZ-001", x=100, y=700)
连续中文/西文分段后分别使用对应字体;提供 horizontal_scale 时可
横向拉伸。
6. stamps —— 内置图像与便捷盖章
mark = psk.make_checkmark_image(size=30, color=(0, 0, 0, 255), stroke=1)
agree = psk.make_agree_image("同意", font_size=14)
psk.stamp_checkmark("表单.pdf", x=200, y=327)
psk.stamp_agree("表单.pdf", x=200, y=227)
7. background —— 扫描件透视
img = psk.remove_background("扫描件.png", threshold=235) # -> RGBA 图像
RGB 三通道均 ≥ threshold 的像素变为完全透明。接受路径、PIL 图像或
字节。
8. compose —— 位图拼接
strip = psk.hconcat([name_img, date_img], gap=8, align="middle")
stack = psk.vconcat([title_img, body_img], gap=4, align="center")
bigger = psk.scale_to_height(small_img, target_height=60)
wider = psk.scale_to_width(small_img, target_width=200)
交叉轴对齐方式:hconcat 用 top/middle/bottom;vconcat 用
left/center/right。输入图像不会被修改。
9. anchor —— 文本锚点定位(无表格线文档)
anchor = psk.find_text("合同.pdf", "签字:") # 第一处匹配
anchors = psk.find_all_text("合同.pdf", "签字:") # 全部匹配
# 可限定 pages=[0,1];用 occurrence=2 选择第 3 处
psk.stamp_at_anchor(
"合同.pdf", "签名.png", anchor,
position=psk.AFTER, # 匹配文字的 AFTER / BELOW / OVER
gap=3,
)
锚点提供 left/top/width/height 与 point(page_height, position, gap) 供自定义几何使用;to_cell() 可转为 TableCell。
10. watermark —— 斜向水印
overlay = psk.render_watermark_overlay(
595, 842, "作废", font_size=72, angle=45,
color=(220, 38, 38, 110), tile=False,
)
psk.stamp_watermark(
"合同.pdf", "作废", pages=[0, 1],
color=(220, 38, 38, 90), tile=True,
)
color 为 RGBA——alpha 直接决定透明度;平铺模式把文字重复布满整页,
否则只在中央放一个。
11. seam —— 骑缝章
results = psk.stamp_seam(
"合同.pdf", "印章.png",
start_page=0,
page_count=None, # None → 覆盖之后所有页
edge=psk.RIGHT, # 或 psk.LEFT
visible_width_pt=None, # 默认印章宽度的 45%
y_center_pt=None, # 默认页面垂直中心(自页面顶部起算)
)
印章按页数切成条,每条部分位于页外,展开拼合后还原完整印章;支持 旋转页面。
12. scheme —— 可配置签署方案
from pdf_sign_kit import PERSONAL, GROUP, DEPARTMENT
# 直接构造方案
scheme = psk.FlowScheme(
levels=(
psk.LevelSpec(level=1, name="起草", mode=PERSONAL),
psk.LevelSpec(level=2, name="会签", mode=GROUP),
psk.LevelSpec(level=3, name="部门会签", mode=DEPARTMENT),
)
)
# 或用工厂:N 个连续等级,指定名称/方式覆盖
scheme = psk.FlowScheme.create(
3,
names={1: "起草", 2: "会签", 3: "部门会签"},
modes={2: GROUP, 3: DEPARTMENT},
)
psk.FlowScheme.default() # 内置 8 级默认方案
PERSONAL:该级所有被指派的人都必须签署;GROUP:任一人签署整级即完成(自动生成sign_group字段);DEPARTMENT:每个部门需要一人签署,所有部门完成才整级完成(使用department_sign_group)。
LevelSpec 与 FlowScheme 不可变、带校验,可通过 to_dict() /
from_dict() 双向序列化。
Initiator 描述文件发起人——要么只留 0 级标记,要么在指定等级参与
签署:
initiator = psk.Initiator(id="I", name="发起人", level=0, require_signature=False)
build_metadata(scheme, assignments, initiator=None) 生成参与者记录;
assignments 为「等级 → 人员描述符列表」({"id", "name"};DEPARTMENT
人员还需 group / group_name)。传入 now=<可调用对象> 可得到
确定性时间戳。
13. workflow —— 签署规则引擎
can, message = psk.workflow.can_sign(metadata, "A")
can, message = psk.workflow.can_sign_level(metadata, "A", target_level=2)
done, message = psk.workflow.all_completed(metadata)
strict = psk.workflow.all_signed_strict(metadata)
引擎执行等级先后、等级内 sign_order、会签/部门会签门控,并对任何
含 "3" 驳回记录的文件整体阻断。
14. identity —— 身份归一化
psk.bare_id("123 - 张三") # -> "123"
psk.identity_matches("00123", "123") # -> True(纯数字,前导零等价)
15. logtable —— 签字日志表
sig_logs = [
{"工号": "A", "姓名": "甲", "等级": "Lv1", "签字时间": "2026-09-28T10:00"},
]
rejection_logs = []
image = psk.render_log_image("合同.pdf", sig_logs, rejection_logs)
buffer = psk.render_log_png_bytes("合同.pdf", sig_logs, rejection_logs)
url = psk.render_log_data_url("合同.pdf", sig_logs, rejection_logs)
# "data:image/png;base64,...."
状态颜色:已签署绿色、待签署橙色、已驳回红色。
16. keywords —— 中文关键词提取
psk.extract_keywords("图纸标注不清晰,请核对") # -> ["图纸", "标注", "清晰", "核对"]
提取 2–4 字中文连续片段,过滤内置停用词;无匹配时回退为截断原文。
17. template —— 可复用签署位置模板
template = psk.PlacementTemplate.capture(
[{"image": "签名.png", "x": 200, "y": 127, "page": 0}],
name="表单A",
)
psk.save_template("表单_a.json", template)
template = psk.load_template("表单_a.json")
operations = template.instantiate({"sign": "新签名.png"})
psk.stamp_batch("另一个文件.pdf", operations)
模板保存的是逻辑引用名 ref 而非具体图片路径;refs 列出所需引用名,
instantiate() 支持 page_offset 页码偏移。
18. exceptions —— 异常体系
PdfSignKitError 为基类,子类包括:InvalidPdfError、
InvalidImageError、PageOutOfRangeError、FontNotFoundError、
PlacementError。
端到端实战:完整签署流程
以下脚本完全自包含、可直接运行:
import os
import tempfile
from datetime import datetime
import fitz
from PIL import Image, ImageDraw
import pdf_sign_kit as psk
from pdf_sign_kit import FlowScheme, Initiator, LevelSpec, PERSONAL, GROUP
work = tempfile.mkdtemp(prefix="capstone_")
pdf_path = os.path.join(work, "contract.pdf")
# 第 1 步 —— 创建两页 PDF,页面上画两个带边框的签署单元格
doc = fitz.open()
page = doc.new_page(width=595, height=842)
page.draw_rect(fitz.Rect(100, 700, 300, 730))
page.draw_rect(fitz.Rect(100, 600, 300, 630))
page.insert_text((100, 690), "Lv1 Sign:", fontsize=10)
page.insert_text((100, 590), "Lv2 Sign:", fontsize=10)
doc.new_page(width=595, height=842)
doc.save(pdf_path)
doc.close()
# 第 2 步 —— 绘制签名并去除白底
sig = Image.new("RGB", (120, 50), (255, 255, 255))
ImageDraw.Draw(sig).line(
[(10, 40), (40, 10), (70, 35), (110, 15)], fill=(0, 0, 0), width=3
)
sig_path = os.path.join(work, "sign.png")
psk.remove_background(sig, threshold=235).save(sig_path)
# 第 3 步 —— 定义两级方案:1 级个人起草,2 级会签
scheme = FlowScheme(
levels=(
LevelSpec(level=1, name="起草", mode=PERSONAL),
LevelSpec(level=2, name="会签", mode=GROUP),
)
)
metadata = build_metadata(
scheme,
{
1: [{"id": "A", "name": "甲"}],
2: [{"id": "B", "name": "乙"}, {"id": "C", "name": "丙"}],
},
initiator=Initiator(id="I", name="发起人"),
)
# 每个单元格中心对应的点击坐标(左下原点)
clicks = {1: (200.0, 842 - 715.0), 2: (200.0, 842 - 615.0)}
# 第 4 步 —— 逐级签署:询问引擎 → 物理盖章 → 更新记录
for level in (1, 2):
pending = [r for r in metadata if r["level"] == level and r["state"] == "0"]
signer = pending[0]
can, message = psk.workflow.can_sign(metadata, signer["person"])
assert can, message
x, y = clicks[level]
psk.stamp(pdf_path, sig_path, x=x, y=y, page=0)
signer["state"] = "1"
signer["sign_time"] = datetime.now().isoformat()
for other in pending[1:]: # 会签组其余成员
other["state"] = "2"
other["sign_group_completed_by"] = signer["person"]
done, message = psk.workflow.all_completed(metadata)
assert done
# 第 5 步 —— 两页骑缝章;首页斜向水印
psk.stamp_seam(pdf_path, sig_path)
psk.stamp_watermark(pdf_path, "作废", pages=[0], color=(220, 38, 38, 80))
# 第 6 步 —— 渲染签字日志
sig_logs = [
{"工号": r["person"], "姓名": r["name"], "等级": f'Lv{r["level"]}',
"签字时间": r["sign_time"]}
for r in metadata if r["role"] == "signer" and r.get("sign_time")
]
log_url = psk.render_log_data_url("contract.pdf", sig_logs, [])
# 第 7 步 —— 完整性哈希;保存/复用签署位置模板
digest = psk.hash_file(pdf_path)
assert psk.verify_file_hash(pdf_path, digest)
template = psk.PlacementTemplate.capture(
[{"image": sig_path, "x": 200.0, "y": 127.0, "page": 0}],
name="contract-form",
)
psk.save_template(os.path.join(work, "tpl.json"), template)
常见问题 / FAQ
- 最小化 Linux 镜像上报
FontNotFoundError—— 安装中文字体 (apt-get install fonts-noto-cjk),或显式传入cjk_font_path/font_path。 find_table_cell返回None—— 该位置没有可检测的边框包围。 改用find_text(文本锚点),或用detect_cell=False配合显式width/height盖章。- 透明印章盖上去后出现白底方框 —— 盖章前先用
remove_background()处理图片。 - 旋转页上盖章位置不对 —— 坐标必须按页面显示方向给出,不要 自己先做旋转换算。
- 会签等级一人签署后没有完成 —— 记录必须带
sign_group(用build_metadata并把该级设为GROUP);只设置sign_type="group"不生效。 PageOutOfRangeError—— 页码从 0 开始,先通过page_count()确认范围。
注意与限制
- 本包提供的是可视化("电子")签名——放置到页面上的光栅图像, 而非基于 PKI/证书、具备法律防抵赖能力的"数字签名"。
- 机打文字以栅格方式写入(不可选中复制)。
- 包不携带字体,在宿主系统上自动探测。
许可证
MIT © 兰衣
Metadata
Release files for pdf-sign-kit 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pdf_sign_kit-0.5.1.tar.gz | 65.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pdf_sign_kit-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 120.2 kB
Release files / pdf_sign_kit-0.5.1.tar.gz
| Download URL | pdf_sign_kit-0.5.1.tar.gz |
|---|---|
| Size | 65.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c7e2c2fa20d7e029dab45fa49c3dddfcceb8366c870bf6e1b57c176db5f2df38
|
|
BLAKE2b-256 checksum How to use checksums |
0eff41dd29fe3ff2ae04bef15c70e26546f026822dd61e4d12f07e51949f4979
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / pdf_sign_kit-0.5.1-py3-none-any.whl
| Download URL | pdf_sign_kit-0.5.1-py3-none-any.whl |
|---|---|
| Size | 54.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ed41916d4990556f44a291849b6a999bfc366bcb92efb2383f8a64385a8c95e2
|
|
BLAKE2b-256 checksum How to use checksums |
34d4eae1b9bff8c3a7484f9124d2d2483bafce173b6ced3229b8a33917f04e72
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|