Skip to main content

pdf-sign-kit

PyPI version Python License: MIT

English | 中文文档


English

pdf-sign-kit is a framework-agnostic Python toolkit for stamping signatures, seals and machine text onto PDF documents. 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", "编号 XX-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(
    "编号 XX-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", "编号 XX-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 (a sign_group field is generated);
  • DEPARTMENT — each department needs one signer; all departments must finish (uses department_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

  • FontNotFoundError on a minimal Linux image — install a CJK font (apt-get install fonts-noto-cjk) or pass cjk_font_path / font_path explicitly.
  • find_table_cell returns None — the point is not enclosed by detectable borders. Use find_text (text anchor), or stamp with detect_cell=False and explicit width / 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 (use build_metadata with a GROUP level); setting only sign_type="group" is insufficient.
  • PageOutOfRangeError — pages are zero-based; check against page_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 文档上加盖 签名、印章与机打文字。库直接操作本地文件:不依赖 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", "编号 XX-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(
    "编号 XX-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", "编号 XX-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.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pdf-sign-kit 0.5.2
File Size Uploaded
pdf_sign_kit-0.5.2.tar.gz 64.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pdf-sign-kit 0.5.2
File Interpreter ABI Platform
pdf_sign_kit-0.5.2-py3-none-any.whl Python 3 none any Details

Total release size: 119.5 kB

Release files / pdf_sign_kit-0.5.2.tar.gz

Download URL pdf_sign_kit-0.5.2.tar.gz
Size 64.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1a5099e9b22b631e5e0b384c468a2895c4f83e42b2c369f7ddcba48717004570
BLAKE2b-256 checksum
How to use checksums
4e1842be8a607745bad63caa2252fc38dd75f17bcc94e767344a51cc57ce3a0f
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.2-py3-none-any.whl

Download URL pdf_sign_kit-0.5.2-py3-none-any.whl
Size 54.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
abb240a592a08ca499148f3235d6212b57d2580ffe895d530264efd6fd9b2101
BLAKE2b-256 checksum
How to use checksums
5d7a328cba2d383cbff3e8b48a9f990a694a1f545b85fc2681c20003f6ac82f3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.10

Release history Release notifications | RSS feed

This release

0.5.2 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page