资讯动态

PDF 脱敏技术【2】:从识别到永久删除:用 Foxit PDF SDK C++ 构建可验证的 PDF 脱敏流程

发布时间:2026/8/13 8:52:29 来源:尧图企业网站定制
上一篇用合成样本验证了三类容易被混为一谈的风险黑框只遮住视觉、不删除底层文字增量保存可能保留旧修订即使文字对象已经删除特定工作流还可能存在字形位置侧信道。本篇不试图一次解决所有问题而是先完成普通文本型 PDF 的最小、可复现闭环解析页面 → 搜索目标 → 检查矩形 → Mark → Apply → 非增量全量另存 → 独立解析与结构检查 → 决定是否发布结论先行Foxit PDF SDK 的MarkRedactAnnot()负责建立待脱敏区域Redaction::Apply()才负责应用标记、移除区域内的页面内容对象生成文件之后还要由独立实现进行验收。搜索成功不等于标记成功标记成功不等于已经删除保存成功也不等于可以发布。先限定本篇处理范围PDF 不只是几页文字。敏感信息还可能出现在扫描图像、OCR 文本层、表单字段、批注、附件、元数据、图层、JavaScript、签名覆盖的旧字节以及增量修订中。本篇只处理一个可解释的基线场景普通文本型、单页、无附件和表单的合成 PDF目标值事先已知。测试值包括合成姓名、手机号、证件号和薪资共 4 项。扫描件、跨行与旋转页面、隐藏对象和对抗性验证分别留给后续文章。这个限制很重要。只有先声明测试范围最后的“通过”才有准确含义。识别值与定位矩形是两个阶段业务系统通常先在规范化文本或结构化数据中识别敏感值再把逻辑目标映射为 PDF 页面上的一个或多个矩形。精确词典适合已知姓名、账号和项目代号正则适合格式较稳定的手机号或证件号固定字段坐标适合模板文档OCR 则用于扫描件。本篇示例的--term使用 FoxitTextSearch的非正则字面量子串搜索默认不区分大小写也不要求完整单词匹配。它适合验证已知值但不能冒充通用实体识别器。例如搜索Ann可能同时命中Anna业务规则如果要求全词、大小写、规范化或正则匹配应显式设置并准备相应测试。另一个容易漏掉的点是一次文本命中可能返回多个矩形。跨行文字、分散字符或复杂排版不能只取第一个矩形。Foxit C 最小实现本文使用 Foxit PDF SDK 11.1、Visual Studio 2022 x64 和 C17。完整示例位于examples/foxit_cpp/redact_terms.cpp由本文独立编写不复制 SDK 包内示例代码也不包含许可证内容。我把SDK包放入到和我的工程同级目录“foxitpdfsdk_11_1_win”下使用试用版本的key.1. 安全读取许可证并初始化示例程序只从当前进程的FOXIT_SN、FOXIT_KEY环境变量读取授权值。本文使用的本地授权文件并不是“整文件即值”序列号应取gsdk_sn.txt中SN后的字符串密钥应取gsdk_key.txt中sign后的字符串。把字段名或说明文字一起传给Library::Initialize()会得到无效许可证错误。下面的 PowerShell 只注入解析后的字段不打印它们$snRawGet-Content.\foxitpdfsdk_11_1_win\lib\gsdk_sn.txt-Raw$keyRawGet-Content.\foxitpdfsdk_11_1_win\lib\gsdk_key.txt-Raw$env:FOXIT_SN [regex]::Match($snRaw,(?im)^\s*SN\s*\s*(\S)\s*$).Groups[1].Value$env:FOXIT_KEY [regex]::Match($keyRaw,(?im)^\s*sign\s*\s*(\S)\s*$).Groups[1].Value生产环境还应检查匹配是否成功并在进程结束时清除变量。许可证不能写进源码、截图、构建日志或代码仓库。C 中用 RAII 保证初始化与释放成对发生conststd::string snReadRequiredEnvironment(FOXIT_SN);conststd::string keyReadRequiredEnvironment(FOXIT_KEY);constErrorCode resultLibrary::Initialize(sn.c_str(),key.c_str());if(result!foxit::e_ErrSuccess){throwstd::runtime_error(Foxit PDF SDK initialization failed; error_codestd::to_string(static_castint(result)));}日志只记录阶段和错误码不记录授权值或目标原文。2. 解析页面并搜索目标PDFPage pagedocument.GetPage(page_index);FinishProgress(page.StartParse(PDFPage::e_ParsePageNormal,nullptr,false),Page parsing);TextPagetext_page(page,TextPage::e_ParseTextNormal);TextSearchsearch(text_page);if(!search.SetSearchFlags(TextSearch::e_SearchNormal)||!search.SetPattern(term,false)){throwstd::runtime_error(Cannot configure text search);}StartParse()返回的是渐进式任务。示例持续调用Continue()直到e_Finished如果收到e_Error任务立即失败不能把“已开始解析”当成“已完成解析”。3. 每次命中都检查矩形和标记结果while(search.FindNext()){RectFArray rectanglessearch.GetMatchRects();if(rectangles.GetSize()0){throwstd::runtime_error(Search returned a match without rectangles);}constautoannotationredaction.MarkRedactAnnot(page,rectangles);if(annotation.IsEmpty()){throwstd::runtime_error(Cannot create redaction annotation);}match_count;}对本文 4 个已知必删值任一目标在全文件零命中都会中止任务。批处理系统可以把规则区分为required和optional但不能默认把漏匹配当成成功。4. Apply 后以固定策略另存if(!redaction.Apply()){throwstd::runtime_error(Redaction apply failed);}constfoxit::uint32 save_flagsPDFDoc::e_SaveFlagNoOriginal|PDFDoc::e_SaveFlagRemoveRedundantObjects;if(!document.SaveAs(output_path.c_str(),save_flags)){throwstd::runtime_error(Cannot save output PDF);}本地fs_redaction.h对Apply()的说明是永久移除标记区域内的文本、图像和路径对象并移除相应 redaction 注释。这里仍要把两件事分开Apply()处理当前有效页面对象非增量全量另存则避免把原文件数据作为发布副本的基础。示例固定组合e_SaveFlagNoOriginal与e_SaveFlagRemoveRedundantObjects不加入e_SaveFlagIncremental同时拒绝覆盖已存在的输出文件。输入路径与输出路径不同只是原件保护措施不能单独证明发生了全量重写。/Prev、多组startxref/%%EOF等属于本文采用的保守修订结构指示器。发现这些指示器时发布准入检查不通过未发现这些指示器不等于已经完成所有历史内容的取证证明。用真实 Foxit 输出做一次完整实验运行参数覆盖合成样本中的 4 个目标.\redact_terms.exe .\pdf_redaction_test_original.pdf .\output\pdf\pdf-redaction-test-foxit-redacted.pdf --termZhang San--term13812345678--term350102199001011234--termCNY 35,000Foxit 处理阶段返回matches4。输出使用新路径原件未被覆盖。重新打开并渲染后4 个字段位置均显示为黑色脱敏区域视觉正确仍然只是第一层证据。随后使用仓库中的 pypdf 验证器python tools/verify_pdf_redaction.py output/pdf/pdf-redaction-test-foxit-redacted.pdf --denylist assets/data/known-sensitive-values.txt --expected-pages 1 --fail-on-attachments --fail-on-forms --fail-on-active-content --fail-on-incremental-updates --json qa/foxit-output-verification.json为了减少“一个解析器漏掉、另一个解析器也没检查”的风险又使用基于 pdfminer 的pdfplumber做第二次文本提取。结果如下检查项实际结果Foxit 搜索并标记的目标4pypdf 拒绝列表命中0pdfminer 拒绝列表命中0startxref / %%EOF / /Prev1 / 1 / 0检测到增量更新false附件 / 表单字段 / 活动内容线索0 / 0 / 0这次实验支持的准确结论是在本文合成样本和 4 个已知目标值的测试范围内Foxit 输出通过视觉、两种独立文本提取、修订结构和重新打开检查。验证器同时给出两项提醒文件仍有文档元数据且存在可选内容属性——本样本包含 Foxit SDK 评估水印。它们没有命中本文拒绝列表但仍应根据发布白名单复核。字形位置侧信道本次没有实现攻击模型状态应记录为not_tested不能默认判定通过。把失败变成可处理状态示例把以下情况全部视为失败并返回非零状态授权字段缺失、格式错误或模块权限不足输入不存在、输出已存在或两个路径相同文档加载、页面解析或搜索配置失败任一必删目标零命中命中没有矩形或 redaction 注释创建失败Apply()或SaveAs()失败独立验证命中拒绝列表或违反必选发布准入规则。批处理服务收到失败后应保留原件、隔离候选输出并记录不含敏感原文的原因不能继续把部分结果送入发布目录。为什么使用 SDK而不是自己改内容流PDF 页面内容涉及字体子集、字符编码、文本矩阵、裁剪、透明度、共享 XObject、压缩流和异常文件。自己改写内容流看似减少依赖进入复杂文档后成本会转移到格式兼容、崩溃恢复和长期回归测试。Foxit 的公开资料说明 Windows SDK 提供多语言 API并支持桌面和服务端集成Redaction add-on 用于移除文本、图形和图像。SDK 可以作为处理引擎但数据识别、人审、发布准入规则、审计和独立验证仍属于完整系统职责。选型时应使用自己的合成样本与风险矩阵做 POC不能只根据功能列表下结论。下一篇将处理文本型 PDF最常见的漏删原因文字被拆成多个对象、姓名跨行、页面旋转以及页面坐标和字形边界不一致。复杂规则不应继续塞进本篇的最小闭环第 3 篇会单独建立“规范化文本 → 原始字符索引 → 多个页面矩形”的字符级回映流程并用相邻控制文字检查误删。本文基于公开资料和个人技术验证不代表福昕官方观点法规内容仅作技术背景不构成法律意见。评论或私信“PDF脱敏清单”获取完整示例代码。如果你需要自己把上述流程放进自己的 Windows 应用或服务端 POC可以查看 Foxit PDF SDK 的试用入口和 Redaction add-on 相关API说明。专栏导航上一篇PDF 脱敏技术【1】:PDF 脱敏不是盖黑框为什么敏感信息仍能被复制正确的保护方式是什么系列目录PDF 脱敏技术系列下一篇[PDF 脱敏技术【3】:PDF 文本脱敏为何总漏字

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价