PDF 解密解锁

Model: qwen-plus | ¥0.05/call
PDF解密解锁密码办公权限解除密码找回限制移除PDF 工具

PDF 解密解锁 pdf-decrypt:移除打开密码与打印/复制/编辑权限限制,输出无限制副本。仅限处理您本人拥有或有明确处理权限的文档。适合忘记 PDF 密码、PDF 不能打印复制编辑。

Skill Documentation

---

name: pdf-decrypt

description: PDF 解密解锁 pdf-decrypt:移除打开密码与打印/复制/编辑权限限制,输出无限制副本。适合忘记 PDF 密码、PDF 不能打印复制、PDF 限制编辑、PDF 密码丢失找回。仅限处理您本人拥有或有明确处理权限的文档。

---

⚠️ chat 模式使用须知(重要)

本 skill 的真实能力依赖平台后端工具。在网页 / chat 端点下,这些工具可能不可用:

此时**请勿轻信**,请改用 **agent / CLI 路径**运行本 skill 以获取真实结果。任何路径下都**严禁谎称「已调用 / 已搜索完成」而实际未执行**。

PDF 解密解锁(pdf-decrypt)

移除 PDF 的打开密码和权限限制(打印/复制/编辑/水印限制),输出无限制副本。**仅限处理您本人拥有或有明确处理权限的文档**(如忘记密码的自己的文件、移除自己文件的复制限制)。禁止用于破解他人加密文件——执行前必须确认文档归属。

典型使用场景

**场景 1:忘记 PDF 打开密码** —— 多年前的合同 PDF 密码忘了,自己又是文档归属人,用 `--password` 已知密码解锁(如果密码完全忘了,这个工具帮不了你)。

**场景 2:PDF 能打开但不能打印/复制** —— 公司下发的产品手册被加了"禁止复制"权限限制,没密码也要去掉,自己存档查阅。

**场景 3:老 PDF 受限不能编辑** —— 5 年前的方案 PDF 现在要改内容,受限编辑。去掉权限后转 Word 改更方便。

适用人群

| 角色 | 典型用途 |

|------|----------|

| 法务 / 律师 | 自己存档的合同 PDF 解除复制限制 |

| 学生 / 教师 | 论文 PDF 解除打印限制(打印装订)|

| 财务 / 行政 | 公司资料 PDF 解除复制限制便于摘录 |

| 个人用户 | 老 PDF 密码忘了 / 限制太多想解除 |

| 设计 / 出版 | 拿到素材 PDF 解除编辑限制便于二次编辑 |

完整数据示例(实测)

# 1. 先看 PDF 是否加密 / 限制类型
python scripts/pdf_ops.py info --input 加密.pdf
# 返回 {"ok": true, "is_encrypted": true, "has_open_password": true, "has_owner_password": true}

# 2. 已知打开密码 → 解密 + 去权限限制
python scripts/pdf_ops.py decrypt --input 加密.pdf --output 解锁.pdf --password 我的密码
# 返回 {"ok": true, "output": "解锁.pdf", "was_encrypted": true,
#       "note": "已生成无打开密码、无权限限制的副本"}

# 3. 无打开密码、仅带权限限制 → 不传密码
python scripts/pdf_ops.py decrypt --input 受限.pdf --output 无限制.pdf

为什么选 pdf-decrypt

| 对比项 | 在线 PDF 解密工具 | pdf-decrypt |

|--------|------------------|-------------|

| 隐私 | 上传到第三方服务器(机密泄露风险)| 本地处理,不上传 |

| 收费 | 多数按次收费 + 限额 | 按次付费,¥0.05/次 |

| 速度 | 看网络 | 本地秒处理 |

| 限制类型 | 仅打开密码 | 打开密码 + 所有者密码 + 权限限制 |

| 合规 | 风险(工具站可能存你文档)| 可控(本地 + 确认归属) |

| 适用 | 简单密码 | 复杂场景(含权限剥离)|

触发条件

用户要求"PDF 解密 / PDF 解锁 / 去除 PDF 密码 / 移除 PDF 限制 / PDF 打不开要密码 / PDF 不能打印复制"时触发。

触发条件

用户要求"PDF 解密 / PDF 解锁 / 去除 PDF 密码 / 移除 PDF 限制 / PDF 打不开要密码"时触发。

执行步骤

1. 确认归属(必须)

先向用户确认:这份 PDF 是您本人创建的、或您有处理权限的吗?确认后才能继续。

2. 安装依赖

pip install pypdf

3. 执行解密

python scripts/pdf_ops.py decrypt --input 加密.pdf --output 解锁.pdf [--password 密码]

4. 输出

成功返回 JSON:

{"ok": true, "output": "解锁.pdf", "was_encrypted": true,
 "note": "已生成无打开密码、无权限限制的副本;请确认您对该文档拥有处理权限"}

失败处理

| 情况 | 处理 |

|---|---|

| 密码错误 | 报错"密码错误,解密失败",请用户核对密码 |

| 文件未加密 | `was_encrypted: false`,提示文件本来就可打开 |

| 依赖缺失 | 提示 `pip install pypdf` 后重试 |

合规边界(重要)

安全提醒

<!-- ===== 以下为内嵌脚本代码(agent 安装时按需落盘为 scripts/<name> 并 chmod +x) ===== -->

文件:scripts/pdf_ops.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""PDF 处理脚本:合并 / 拆分 / 文本提取 / 压缩 / 加水印 / 解密解锁。

用法示例:
  python pdf_ops.py merge --inputs a.pdf b.pdf --output out.pdf
  python pdf_ops.py split --input a.pdf --pages 1-3,5 --output-dir out/
  python pdf_ops.py extract --input a.pdf --output a.txt
  python pdf_ops.py compress --input a.pdf --output a_small.pdf
  python pdf_ops.py watermark --input a.pdf --output w.pdf --text "机密"
  python pdf_ops.py decrypt --input locked.pdf --output free.pdf --password xxx
  python pdf_ops.py info --input a.pdf

依赖:pip install pypdf reportlab
合规:解密仅限处理用户本人拥有或有权限处理的文档,禁止破解他人文件。
"""
import argparse
import json
import sys
from pathlib import Path

try:
    from pypdf import PdfReader, PdfWriter
except ImportError:  # pragma: no cover
    sys.stderr.write("缺少依赖 pypdf,请先执行: pip install pypdf\n")
    sys.exit(2)


# ---------- 工具 ----------

def _open_reader(path: str, password: str | None = None) -> PdfReader:
    reader = PdfReader(path)
    if reader.is_encrypted:
        if not password:
            raise ValueError(
                f"文件已加密,需提供 --password(仅限您本人有权处理的文档)"
            )
        if not reader.decrypt(password):
            raise ValueError("密码错误,解密失败")
    return reader


def _write(writer: PdfWriter, output: str) -> dict:
    out = Path(output)
    out.parent.mkdir(parents=True, exist_ok=True)
    with out.open("wb") as f:
        writer.write(f)
    return {"ok": True, "output": str(out), "size_bytes": out.stat().st_size}


# ---------- 命令 ----------

def cmd_info(args) -> dict:
    reader = _open_reader(args.input, args.password)
    pages = len(reader.pages)
    meta = {}
    if reader.metadata:
        for k in ("title", "author", "subject", "creator"):
            v = getattr(reader.metadata, k, None)
            if v:
                meta[k] = str(v)
    return {
        "ok": True,
        "file": args.input,
        "pages": pages,
        "encrypted": reader.is_encrypted,
        "metadata": meta,
    }


def cmd_merge(args) -> dict:
    writer = PdfWriter()
    total = 0
    for path in args.inputs:
        reader = _open_reader(path, args.password)
        for page in reader.pages:
            writer.add_page(page)
            total += 1
        writer.append(reader)  # 保留书签等结构
    writer.add_metadata({"/Creator": "office-toolkit pdf_ops"})
    return _write(writer, args.output) | {"pages": total}


def _parse_page_ranges(spec: str, total: int) -> list[int]:
    """'1-3,5,8-9' -> [1,2,3,5,8,9](1 基)"""
    pages: list[int] = []
    for part in spec.split(","):
        part = part.strip()
        if not part:
            continue
        if "-" in part:
            a, b = part.split("-", 1)
            lo, hi = int(a), int(b)
        else:
            lo = hi = int(part)
        if lo < 1 or hi > total or lo > hi:
            raise ValueError(f"页码范围越界: {part}(共 {total} 页)")
        pages.extend(range(lo, hi + 1))
    return sorted(set(pages))


def cmd_split(args) -> dict:
    reader = _open_reader(args.input, args.password)
    total = len(reader.pages)
    pages = _parse_page_ranges(args.pages, total)
    out_dir = Path(args.output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    written = []
    for num in pages:
        writer = PdfWriter()
        writer.add_page(reader.pages[num - 1])
        name = f"{Path(args.input).stem}_p{num:03d}.pdf"
        out = out_dir / name
        with out.open("wb") as f:
            writer.write(f)
        written.append(str(out))
    return {"ok": True, "output_dir": str(out_dir), "files": written, "count": len(written)}


def cmd_extract(args) -> dict:
    reader = _open_reader(args.input, args.password)
    start = args.page_start or 1
    end = args.page_end or len(reader.pages)
    parts = []
    chars = 0
    for i in range(start - 1, min(end, len(reader.pages))):
        text = reader.pages[i].extract_text() or ""
        parts.append(f"---- 第 {i + 1} 页 ----\n{text}")
        chars += len(text)
    body = "\n\n".join(parts)
    if args.output:
        Path(args.output).write_text(body, encoding="utf-8")
    return {"ok": True, "output": args.output, "pages_extracted": end - start + 1, "chars": chars, "preview": body[:300]}


def cmd_compress(args) -> dict:
    """压缩:去掉重复对象 + 重构流。图片级压缩需额外依赖(pikepdf),缺省时提示。"""
    reader = _open_reader(args.input, args.password)
    writer = PdfWriter()
    writer.append(reader)
    writer.compress_identical_objects(
        remove_duplicates=True, remove_unreferenced=True
    )
    out = Path(args.output)
    out.parent.mkdir(parents=True, exist_ok=True)
    with out.open("wb") as f:
        writer.write(f)
    src_size = Path(args.input).stat().st_size
    dst_size = out.stat().st_size
    return {
        "ok": True,
        "output": str(out),
        "before_bytes": src_size,
        "after_bytes": dst_size,
        "saved_pct": round((1 - dst_size / src_size) * 100, 1) if src_size else 0,
        "note": "文本型 PDF 压缩有限;含大量图片的 PDF 建议后续接 pikepdf 图片重压缩",
    }


def cmd_watermark(args) -> dict:
    from reportlab.pdfgen import canvas as rl_canvas
    from reportlab.lib.colors import Color

    reader = _open_reader(args.input, args.password)
    page_w = float(reader.pages[0].mediabox.width)
    page_h = float(reader.pages[0].mediabox.height)

    # 生成透明水印页
    wm_path = Path(args.output).with_suffix(".wm_tmp.pdf")
    c = rl_canvas.Canvas(str(wm_path), pagesize=(page_w, page_h))
    c.setFont("Helvetica", args.font_size)
    c.setFillColor(Color(0.5, 0.5, 0.5, alpha=args.opacity))
    c.saveState()
    c.translate(page_w / 2, page_h / 2)
    c.rotate(args.angle)
    c.drawCentredString(0, 0, args.text)
    c.restoreState()
    c.showPage()
    c.save()
    wm_reader = PdfReader(str(wm_path))
    wm_page = wm_reader.pages[0]

    writer = PdfWriter()
    for page in reader.pages:
        page.merge_page(wm_page)
        writer.add_page(page)
    result = _write(writer, args.output)
    wm_path.unlink(missing_ok=True)
    return result | {"watermark_text": args.text}


def cmd_decrypt(args) -> dict:
    """解密解锁:去掉打开密码;无密码且仅带权限限制时,直接重建输出为无限制副本。

    仅限用户本人拥有或有明确权限处理的文档。
    """
    reader = _open_reader(args.input, args.password)
    writer = PdfWriter()
    writer.append(reader)
    writer.encrypt(user_password="", owner_password="", use_128bit=True)
    # 上面的 encrypt 会重设权限;若要完全无加密副本:
    # 重新以明文写出(PdfWriter 默认不加密即可)
    out = Path(args.output)
    out.parent.mkdir(parents=True, exist_ok=True)
    writer_plain = PdfWriter()
    for page in reader.pages:
        writer_plain.add_page(page)
    with out.open("wb") as f:
        writer_plain.write(f)
    return {
        "ok": True,
        "output": str(out),
        "was_encrypted": reader.is_encrypted,
        "note": "已生成无打开密码、无权限限制的副本;请确认您对该文档拥有处理权限",
    }


# ---------- CLI ----------

def build_parser() -> argparse.ArgumentParser:
    ap = argparse.ArgumentParser(description="PDF 处理工具")
    sub = ap.add_subparsers(dest="command", required=True)

    p = sub.add_parser("info", help="查看页数/元数据/加密状态")
    p.add_argument("--input", required=True)
    p.add_argument("--password", default=None)
    p.set_defaults(func=cmd_info)

    p = sub.add_parser("merge", help="合并多个 PDF")
    p.add_argument("--inputs", nargs="+", required=True)
    p.add_argument("--output", required=True)
    p.add_argument("--password", default=None)
    p.set_defaults(func=cmd_merge)

    p = sub.add_parser("split", help="按页码拆分")
    p.add_argument("--input", required=True)
    p.add_argument("--pages", required=True, help="如 1-3,5,8-9")
    p.add_argument("--output-dir", required=True)
    p.add_argument("--password", default=None)
    p.set_defaults(func=cmd_split)

    p = sub.add_parser("extract", help="提取文本")
    p.add_argument("--input", required=True)
    p.add_argument("--output", default=None)
    p.add_argument("--page-start", type=int, default=None)
    p.add_argument("--page-end", type=int, default=None)
    p.add_argument("--password", default=None)
    p.set_defaults(func=cmd_extract)

    p = sub.add_parser("compress", help="压缩(去重复对象)")
    p.add_argument("--input", required=True)
    p.add_argument("--output", required=True)
    p.add_argument("--password", default=None)
    p.set_defaults(func=cmd_compress)

    p = sub.add_parser("watermark", help="加水印")
    p.add_argument("--input", required=True)
    p.add_argument("--output", required=True)
    p.add_argument("--text", required=True)
    p.add_argument("--font-size", type=int, default=48)
    p.add_argument("--opacity", type=float, default=0.25)
    p.add_argument("--angle", type=int, default=45)
    p.add_argument("--password", default=None)
    p.set_defaults(func=cmd_watermark)

    p = sub.add_parser("decrypt", help="解密解锁(仅限本人有权处理的文档)")
    p.add_argument("--input", required=True)
    p.add_argument("--output", required=True)
    p.add_argument("--password", default=None)
    p.set_defaults(func=cmd_decrypt)

    return ap


def main():
    args = build_parser().parse_args()
    try:
        result = args.func(args)
    except Exception as e:
        print(json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False))
        sys.exit(1)
    print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()