ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

1.1 清印 ClearMark — 一款本地文档去水印工作台的完整设计与实现

1.1 清印 ClearMark — 一款本地文档去水印工作台的完整设计与实现 1.1 清印 ClearMark — 一款本地文档去水印工作台的完整设计与实现系列第 1 篇 · 共 12 篇这不是一篇产品软文而是一名一线开发者对自己做过的一个工具系统的复盘。从产品定位、架构选型到 PDF 内容流解析、扫描件像素级水印检测、OpenCV 图像修复、PySide6 桌面 UI、跨平台适配、批量任务编排——我会把整套设计拆成 12 篇逐步讲清楚。如果你只是想要个能用的工具可以直接拉到文末下载链接如果你想知道水印到底是怎么被检测和擦除的欢迎跟着系列一路读下去。一、为什么我们要再做一款去水印工具市面上并不缺去水印工具。在线版的有 ILovePDF、SmallPDF桌面端的有 WPS 自带的去水印、Adobe Acrobat 的编辑功能还有一堆 PDF 转换类网站顺手提供。但真到了实际生产环境你会发现它们都卡在三类痛点上痛点 1在线工具过不了合规审查金融、政企、科研单位里文档大多含合同条款、客户数据、内部资料。把这些文件传到一个境外服务器去做水印识别合规这一关根本过不去。哪怕服务方承诺处理完即删审计也写不出来。我自己就遇到过一份扫描件采购合同上面盖了仅供内部使用的灰色斜铺水印业务方想拿去做汇报。问了一圈在线工具没有一个敢传。最后只能截图——截图又把 OCR 识别率拉低了 30%。痛点 2通用工具识别不全WPS 的去水印只认页眉页脚里的几个固定模板Acrobat 的编辑 PDF可以删文字但你得手动一个个点Photoshop 能修图但不能直接处理 PDF 内容流而且对扫描件烤进去的浅色斜字根本无能为力——那是像素不是图层。最让人头疼的是扫描件。很多公司用扫描全能王、福昕、WPS 扫描后导出 PDF水印是烤进扫描图里的——文字被光栅化成像素浅灰色、半透明、45° 斜铺整页覆盖。这种水印既不是 PDF 注释也不是 XObject是图片像素本身。绝大多数工具遇到它只能放弃。痛点 3批量处理无从下手实际场景里没人会一次处理一份。法务一周要清理几十份合同研究院要批量脱敏几百页报告。这类需求需要一次拖入整个文件夹并发检测不要逐个等每份文件单独列出候选可以人工勾选失败的不影响其他文件输出有迹可循不覆盖原文件市面上 99% 的工具只支持单文件、单水印、单点删除。批量不存在的。二、清印 ClearMark 是什么基于上述痛点我们做了清印 ClearMark 智能文档去水印工作台 V2.0。一句话定位一款 100% 本地运行、支持 PDF 与图片、覆盖矢量/扫描/混合三类水印、可批量的桌面去水印工具。它的核心约束有三条不联网。所有解析、检测、修复、保存都在本地完成。源文件不上传任何服务器连个 ping 都没有。不动源文件。输出永远是新文件存到源目录的output/子文件夹不可写时回退到~/.clearmark/output/同时源文件打开时自动备份到backup/。可解释。每条检测到的水印都会列出候选标注置信度、坐标、来源重复/旋转/签名/聚类/OCR用户勾选才移除——不做黑盒一键清除。它能处理什么水印经过两轮迭代V2.0 覆盖的水印类型如下大类子类检测方式移除策略矢量 PDF 文字水印跨页重复、同页平铺网格、旋转大字、品牌签名内容流解析 几何特征 规则库关键词内容流区间擦除矢量 PDF 图片水印Logo、二维码、共享 XObjectXObject xref 跨页统计整对象清空空流 GC矢量 PDF 图层水印OCG 可选内容图层doc.get_ocgs()写入/D/OFF矢量 PDF 注释水印Watermark/Stamp/FreeText 注释page.annots()遍历page.delete_annot()扫描件浅色斜铺文字烤进扫描图的半透明斜字像素级笔画分割 形态学分组 整页旋转 OCR 验证邻域最大亮度填充 像素掩膜修复扫描件颜色聚类层灰色半透明覆盖K-Means 颜色聚类Alpha 反演修复图片水印文字、Logo、灰色覆盖层OCR 颜色聚类 用户涂抹Telea 修复 Alpha 反演用户手动标记矩形框选、画笔涂抹、套索用户交互矩形掩膜 修复这张表是整个系列后续 10 篇文章的索引。每一行都对应一类检测器和一个移除算法背后都有真实调试故事。三、技术栈为什么这么选工欲善其事必先利其器。但选器这件事比做事本身更费心。我们最终的技术栈是维度选型选它的理由语言Python 3.10.11PDF/图像生态最完整开发效率高便于交付源码GUI 框架PySide6Qt6 官方 Python 绑定跨平台一致、QPainter 渲染能力强、信号槽机制天然适配后台线程PDF 解析PyMuPDFfitz同时支持内容流读写、XObject 操作、注释操作、页面渲染API 比 pypdf 完整得多图像处理OpenCV NumPyK-Means 聚类、形态学操作、Telea/NS 修复算法都是 OpenCV 原生支持OCR 引擎RapidOCRPaddleOCR ONNX 版离线包可独立分发不依赖 PaddlePaddle 大包识别中文稳定内容流解析自研content_stream.pyPyMuPDF 提供的get_texttrace不暴露字节偏移无法做区间擦除必须自己写打包PyInstaller onedir 模式绿色免安装整个文件夹拷到目标机器即可运行目标平台银河麒麟 V10 / Windows 11国产化适配 主流桌面为什么不上 PaddleOCR 全量包因为它体积大700MB而 RapidOCR 只用 ONNX runtime 推理模型 30MB识别中文精度足够。这套组合在麒麟 V10 ARM64 上也能跑起来。为什么不用 pdfplumber / pypdf因为它们不支持修改。去水印的本质是写操作——你要从内容流里删一段、要清空 XObject、要改 OCG 状态这些都需要写权限。PyMuPDF 是少数能稳定支持 PDF 写操作的库。为什么不用 PyQt 而用 PySide6许可证。PySide6 是 LGPL商用更友好API 与 PyQt 几乎一致迁移成本为零。四、系统总览三层架构整个系统分为三层每层职责清晰、互不耦合┌─────────────────────────────────────────────────────────┐ │ UI 层ui/ │ │ ┌──────────────┬──────────────┬────────────────────┐ │ │ │ MainWindow │ PreviewWidget│ ResultPanel │ │ │ │ 三栏布局 │ QPainter渲染 │ 候选列表勾选 │ │ │ ├──────────────┼──────────────┼────────────────────┤ │ │ │ Worker线程 │ Icons多分辨率│ Styles 主题 │ │ │ └──────────────┴──────────────┴────────────────────┘ │ └────────────────────────┬────────────────────────────────┘ │ 统一数据模型 ┌────────────────────────┴────────────────────────────────┐ │ 核心层core/ │ │ ┌──────────┬──────────┬──────────┬─────────────────┐ │ │ │ Session │TaskOrch │FormatProb│ ContentStream │ │ │ │单文件会话 │批量编排 │格式路由 │ 内容流解析器 │ │ │ ├──────────┼──────────┼──────────┼─────────────────┤ │ │ │ Processor│ Detect/ │ Inpaint/ │ Rules/ │ │ │ │三类处理器 │ 5个检测器 │ CV修复引擎│ 规则库 │ │ │ └──────────┴──────────┴──────────┴─────────────────┘ │ └────────────────────────┬────────────────────────────────┘ │ ┌────────────────────────┴────────────────────────────────┐ │ 数据层 │ │ WatermarkCandidate · DetectionResult · TaskItem │ │ OutputStrategy · UserMark │ └─────────────────────────────────────────────────────────┘数据层统一数据模型是关键整个系统最关键的一个设计是core/model.py里的WatermarkCandidatedataclassclassWatermarkCandidate:检测到的水印候选 统一表达所有格式的水印检测结果UI 层只消费此结构。 kind:WatermarkKind# 水印种类page_index:int0bbox:Tuple[float,float,float,float](0,0,0,0)origin:Tuple[float,float](0,0)rotation:float0.0detail:strconfidence:float0.0confidence_level:ConfidenceLevelConfidenceLevel.NONE# 内容流相关PDF 矢量用start:int-1# 内容流字节偏移end:int-1xref:int-1# XObject xreftext:strcolor:Optional[Tuple[float,float,float]]Nonefont_size:float0.0alpha:float1.0# 用户控制selected:boolTrue# 网格信息平铺水印用grid_rows:int0grid_cols:int0grid_angle:float0.0# 运行时像素掩膜扫描图/图片处理器用mask:Optional[object]None它的精妙之处在于所有格式的所有检测结果都被统一表达成这一个结构。PDF 矢量文字水印用start/end存内容流偏移扫描件浅色斜铺水印用mask存像素掩膜规则库命中用text存关键词旋转大字水印用rotation存角度。UI 层ResultPanel只需要遍历candidates列表就能渲染所有候选完全不需要知道背后是 PDF 还是图片。这个数据结构是整个系统的通用货币后续 11 篇文章都会反复提到它。处理器层策略模式 工厂路由core/format_probe.py是入口defprobe_format(path:str)-FormatType:探测文件格式类型 对于 PDF进一步判断是矢量型、扫描型还是混合型。 extget_extension(path)ifextinPDF_EXTS:return_probe_pdf_type(path)elifextinIMAGE_EXTS:returnFormatType.IMAGE...def_probe_pdf_type(path:str)-FormatType:判断 PDF 类型矢量 / 扫描 / 混合docfitz.open(path)has_textFalsehas_full_page_imageFalsehas_vectorFalsecheck_pagesmin(doc.page_count,5)foriinrange(check_pages):pagedoc[i]textpage.get_text(text).strip()iftext:has_textTrueimagespage.get_images(fullTrue)forimginimages:xrefimg[0]pixfitz.Pixmap(doc,xref)# 图片面积接近页面面积 → 扫描件ifpix.width*pix.heightpage.rect.width*page.rect.height*0.8:has_full_page_imageTruedoc.close()ifhas_full_page_imageandnothas_text:returnFormatType.PDF_SCANNEDelifhas_full_page_imageandhas_text:returnFormatType.PDF_MIXEDelse:returnFormatType.PDF_VECTOR注意这里有一个三条腿走路的策略矢量 PDF有文字指令、无大图→PdfVectorProcessor走内容流解析路线可无损擦除扫描 PDF无文字指令、有整页大图→PdfScannedProcessor走像素级修复路线混合 PDF既有文字又有大图→ 走矢量处理器的混合策略路由器在get_processor(format_type)里完成实例化defget_processor(format_type:FormatType):ifformat_typein(FormatType.PDF_VECTOR,FormatType.PDF_MIXED):from.processor.pdf_vectorimportPdfVectorProcessorreturnPdfVectorProcessor()elifformat_typeFormatType.PDF_SCANNED:from.processor.pdf_scannedimportPdfScannedProcessorreturnPdfScannedProcessor()elifformat_typeFormatType.IMAGE:from.processor.image_inpaintimportImageInpaintProcessorreturnImageInpaintProcessor()...所有处理器继承同一个抽象基类IProcessorclassIProcessor(ABC):abstractmethoddefopen(self,path:str):...abstractmethoddefdetect_auto(self)-DetectionResult:...abstractmethoddefdetect_region(self,marks:List[UserMark])-DetectionResult:...abstractmethoddefdetect_preset(self,rule_name:str)-DetectionResult:...abstractmethoddefremove(self,candidates,output_path,progress_callbackNone)-int:...abstractmethoddefrender_page(self,page_index:int,zoom:float1.0)-bytes:...UI 层和会话管理器只需要调processor.detect_auto()/processor.remove(...)根本不需要知道背后是哪种格式。这套设计让后续增加新格式比如 Word、PPT只需要新增一个IProcessor子类不改 UI 层一行代码。五、三类处理器三套坐标系整个系列最绕的地方是坐标契约。三类处理器用三套不同的坐标系处理器坐标系bbox 含义PdfVectorProcessorPDF 页面点page.rect文字块/图片块在页面上的位置PdfScannedProcessor内嵌图像素候选在水印所在 XObject 内嵌图中的像素位置ImageInpaintProcessor原图像素候选在原图上的像素位置为什么扫描件处理器要用内嵌图像素而不是页面点因为扫描件的水印是烤进图片像素的检测和修复都必须在像素空间进行。但 UI 上要显示红框又得换算回页面点。所以PdfScannedProcessor重写了基类的candidate_page_bboxdefcandidate_page_bbox(self,cand)-tuple:内嵌图像素候选 bbox → 页面点坐标供 UI 在渲染图上定位infonext((iforiininfosifi[xref]cand.xref),None)img_w,img_hinfo[width],info[height]px0,py0,px1,py1info[bbox]place_wpx1-px0 place_hpy1-py0 x0px0cand.bbox[0]/img_w*place_w y0py0cand.bbox[1]/img_h*place_h...这套换算在 UI 渲染时被频繁调用。坐标用错一个量级红框就会跑到页面外。我们在第 7 篇 UI 篇会专门讲这个坑。六、UI 层三栏工作台界面布局遵循左导航 中工作区 右详情的经典三栏┌─────────────────────────────────────────────────────────┐ │ 工具栏打开 检测 移除 工具切换 笔刷 高亮 对比 设置 手册 │ ├──────────┬──────────────────────────────┬──────────────┤ │ 左栏 │ 中栏 PreviewWidget │ 右栏 │ │ 文件/ │ ┌──────────────────────────┐ │ ResultPanel │ │ 任务列表 │ │ QPainter 渲染页面 │ │ 候选列表 │ │ │ │ 红框标记水印 │ │ 勾选/取消 │ │ 文件A │ │ 鼠标框选/画笔涂抹 │ │ 置信度 │ │ 文件B │ │ 前后对比双图并排 │ │ 来源标签 │ │ 文件C │ └──────────────────────────┘ │ │ │ │ │ 全选/全不选 │ │ │ │ 移除并保存 │ ├──────────┴──────────────────────────────┴──────────────┤ │ 状态栏当前操作 页码 文件路径 │ └─────────────────────────────────────────────────────────┘操作按钮全选/全不选/移除并保存固定在右栏底部永不随滚动条消失这是 V2.0 改了一版才确定的交互——第一版把按钮放在列表头部结果列表一长按钮就找不到了被同事吐槽过。预览区用QPainter手绘——不用QLabelQPixmap的简单方案因为我们要在 pixmap 上叠加红框、用户框选轨迹、画笔轨迹、对比双图这些都需要 painter 灵活控制 z-order。具体实现见第 7 篇。七、批量处理状态机 运行守卫批量是 V2.0 的重点。core/task_orchestrator.py用ThreadPoolExecutor并发处理defdetect_all(self,progress_callbackNone,cancel_checkNone)-None:self._cancelledFalsepending[tfortinself._tasksift.statusin(TaskStatus.PENDING,TaskStatus.FAILED)]totallen(pending)iftotal0:returndone0withThreadPoolExecutor(max_workersself._max_workers)asexecutor:future_map{executor.submit(self._detect_one,t):tfortinpending}forfutureinas_completed(future_map):if(cancel_checkandcancel_check())orself._cancelled:self._cancelledTrueforfinfuture_map:f.cancel()break...每个TaskItem都有自己的状态机PENDING → DETECTING → DETECTED → REMOVING → COMPLETED失败转到FAILED无水印转到SKIPPED。UI 上的列表项按状态显示不同后缀比如3 候选/1 移除/失败权限不足。UI 线程守卫是核心坑批量按钮必须在执行期间禁用否则用户连点会触发多个 worker 同时操作同一个文件结果就是 core dump。关闭窗口时也要wait()后台线程结束才能退出不然 Qt 会在析构时崩溃。这套坑我们在第 9 篇详细讲。八、跨平台一套代码跑麒麟 V10 Windows 11最后是工程化部分。我们的目标是同一份源码在 Windows 11 和银河麒麟 V10 上都能直接跑不需要任何条件编译。关键策略维度实现字体main._pick_default_font()启动时按平台选WinMicrosoft YaHei UImacOSPingFang SCLinuxNoto Sans CJK SC→WenQuanYi→系统默认打开目录Windows 调explorermacOS 调openLinux 调xdg-openLinux 任务栏分组icons.ensure_linux_desktop_file用sys.executable写.desktop文件路径分隔符全部用os.path.join/os.pathsep不硬编码文件编码所有open()显式encodingutf-8避免 Windows 默认 GBKPyInstaller 打包build.py用os.pathsep自动适配--add-data分隔符Windows 附.icoLinux 附.desktop这一套适配在第 11 篇专门讲踩过的坑够写一篇长文了。九、本系列后续文章索引为了让读者按需阅读本系列共 12 篇按以下顺序更新【开篇】为什么我们要做一款本地文档去水印工具本文【架构】多格式文档处理系统分层架构与处理器路由【PDF 矢量 · 上】手写 PDF 内容流解析器从字节流到结构化水印候选【PDF 矢量 · 下】矢量水印多策略检测与无损移除【扫描件】烤入扫描图的浅色斜铺文字水印像素级 OCR 验证与掩膜修复【图片】通用图像去水印颜色聚类分离 Alpha 反演修复【UI · 上】PySide6 三栏工作台QPainter 渲染、坐标契约与框选/涂抹交互【UI · 下】前后对比模式与多分辨率图标系统【批量】多文件并发任务编排ThreadPoolExecutor 运行守卫 状态机【规则库】扫描全能王/WPS/福昕水印规则库JSON DSL 关键词匹配 视觉特征加权【跨平台】一套代码适配银河麒麟 V10 与 Windows 11【避坑实录】那些深夜调试的坑drawPixmap 崩溃、update_stream 黑块、多次移除白板、高 DPI 警告十、下载与资源清印 ClearMark V2.0 提供源码与可执行程序两种发行方式源码版本包含全部 30 个 Python 文件、规则库、图标资源、打包脚本可直接python main.py运行适合二次开发与学习。绿色免安装版基于 PyInstaller onedir 模式打包整个文件夹拷到目标机器即可运行已包含 Python 运行时与全部依赖适合直接使用。适用平台Windows 10 / 11 (x64)银河麒麟 V10 桌面版x86_64 / aarch64macOS实验性支持下载地址下载链接将在系列文章全部发布后统一更新。如果你正在做以下事情这套源码会对你有帮助想了解 PDF 内容流content stream的结构与解析方式想学习 PyMuPDF 的高级 APIXObject、OCG、注释、replace_image想实践 PySide6 QPainter 的桌面应用开发想研究 OpenCV 图像修复Telea/NS 算法、K-Means 聚类、形态学操作想做跨平台Linux Windows桌面工具的工程化想了解批量并发任务编排与 Qt 线程守卫的最佳实践写在最后做这个工具的过程里最让我感慨的是水印这件事看起来是个产品功能本质上是一组 PDF 与图像算法的工程化整合。从内容流解析、几何变换、形态学、聚类、OCR、图像修复到线程模型、坐标契约、跨平台——每一块单独都能写一本书。把它们整合成一个能用的桌面工具靠的是工程取舍什么时候用启发式、什么时候上 OCR、什么时候交给用户手动框选、什么时候放弃自动化只做半自动。这个系列我想做的不是营销而是把每一块的设计取舍讲清楚。如果你也在做类似的工具或者只是好奇 PDF 内部到底长什么样欢迎跟着读下去。下一篇文章我们会聊整体架构与处理器路由——为什么是三层、为什么是策略模式、IProcessor接口怎么设计才让 UI 层不感知格式差异。作者注本系列基于真实项目开发过程所有代码、调试故事、坑点均为一手记录。文中代码片段均为项目实际实现对应文件路径会在文中标注。如果你希望提前拿到完整源码可以从文末下载链接获取。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进