PDF 解析

PDF 看着是横向,提取坐标却不对:把旋转矩阵说清楚

解析PDF旋转页面坐标错位问题,详解PyMuPDF中未旋转空间与显示空间的矩阵换算。提供内存合成示例与验收清单,说明如何在文档翻译工作流中利用OmniDoc评估复杂版面还原效果。

阅读文本 / Reading text
本文使用合成样本演示处理方法。代码已通过隔离环境中的示例检查;这不代表对所有真实文件或产品效果的保证。处理重要文件前,请保留原件,并按文中的边界与验收项复核。 你正在解析一份扫描件或工程图纸,肉眼看到表格在页面右侧,但 get_text() 返回的坐标却指向左上角;或者试图用代码给某个段落加高亮标注,结果黄色色块跑到了页眉位置。这通常不是解析库的 Bug,而是 PDF 坐标系与视觉渲染不一致造成的。PDF 规范允许页面携带 /Rotate 属性(0、90、180、270度),阅读器在渲染时会自动应用这个旋转,但底层内容流的坐标依然基于未旋转的原始 MediaBox。如果你直接使用提取到的原始坐标进行绘图或区域匹配,就会发生错位。 理解未旋转空间与显示空间的差异 PyMuPDF 从 v1.17.0 开始明确区分了这两种坐标系。 Page.get_text() 、 Page.get_image_bbox() 以及所有注释对象的坐标,默认都相对于 未旋转页面 返回。这意味着无论 PDF 阅读器把页面转了多少度,这些 API 给出的数值始终锚定在原始的宽高上。只有 Page.rect 和 Page.bound() 会反映旋转后的实际显示尺寸。 这种设计是为了保证内容提取的稳定性:即使你修改了页面旋转角度,同一段文字的提取坐标也不会变。但当你需要将这些坐标“画”回页面,或者与鼠标点击等视觉交互对齐时,就必须手动完成坐标变换。PyMuPDF 提供了两个关键矩阵属性: Page.rotation_matrix 用于将未旋转坐标转换为显示坐标, Page.derotation_matrix 则执行逆向操作。这两个矩阵封装了平移和旋转变换,避免了开发者手写三角函数计算。 合成示例:坐标变换的正确与错误预期 为了演示这一机制,我们构造一个纯内存中的合成场景,无需依赖外部文件。若你需要测试真实文档,请提前准备一份包含 /Rotate=90 属性的 PDF 文件,并确保其 CropBox 与 MediaBox 一致,以排除裁切偏移的干扰。 输入设定 :创建一个 A4 纵向页面(595 x 842 pt),设置 /Rotate = 90 。在未旋转空间中,点 (100, 100) 位于左上角附近;但在旋转90度后的显示空间中,由于坐标系随之旋转,该点在视觉上的位置会发生显著变化。 正确输出预期 :将 (100, 100) 乘以 rotation_matrix 后,得到的显示坐标应当落在旋转后页面的有效边界(即 page.rect )内。对于默认的 A4 页面逆时针旋转90度,原左上角的点在显示空间中应变为左下角附近,即 x 坐标较大而 y 坐标接近 0。 切勿盲目假设 y > x ,具体数值完全取决于 MediaBox 的原点定义。 边界情况处理 :当旋转角度为 0 时, rotation_matrix 应为单位矩阵。若传入非 90 度倍数的非法旋转值,PyMuPDF 通常会归一化处理,但防御性编程仍建议检查矩阵有效性,避免产生 NaN 坐标。 Python 实现坐标校验逻辑 以下示例展示如何在内存中验证坐标变换。请注意,此代码仅用于演示矩阵运算逻辑,并非完整的 PDF 处理管线。运行前需确保安装了 PyMuPDF 库,并在你自己的锁定依赖环境中执行。 import fitz # PyMuPDF def validate_rotation_transform(): """合成示例:验证旋转矩阵在未旋转坐标与显示坐标间的转换""" doc = fitz.open() # 空文档,无需文件路径 page = doc.new_page(width=595, height=842) # 设置页面旋转90度,模拟横向显示的PDF page.set_rotation(90) # 定义一个在未旋转空间中的测试点 (左上角附近) unrotated_point = fitz.Point(100, 100) # 获取旋转矩阵并执行变换 mat = page.rotation_matrix display_point = unrotated_point * mat # 验证1: 变换后的点必须位于旋转后的页面可视区域内 assert page.rect.contains(display_point), ( f"变换后坐标 {display_point} 超出显示边界 {page.rect}" ) # 验证2: 对于默认A4逆时针旋转90度,原(100,100)应变为左下角 # 即 x 接近页宽(842),y 接近 0。注意:此处纠正了常见的 y>x 误区 assert display_point.x > 500 and display_point.y < 200, ( f"90度旋转预期左下角,实际得到 ({display_point.x:.1f}, {display_point.y:.1f})" ) # 验证3: 逆变换可精确恢复原始坐标 inv_mat = page.derotation_matrix recovered = display_point * inv_mat assert abs(recovered.x - unrotated_point.x) < 0.01 assert abs(recovered.y - unrotated_point.y) < 0.01 doc.close() print("坐标变换逻辑校验通过(合成数据)") # 请在本地环境运行,不要假设线上环境已安装fitz validate_rotation_transform() 这段代码没有读取任何真实文件,完全在内存中构造页面状态。 assert 语句检查的是数学关系的合理性,而非业务正确性。如果你的实际 PDF 包含非标准 MediaBox 或嵌套变换,可能需要额外调试。 验收标准与人工审查边界 代码能通过断言不代表生产环境万无一失。下表列出常见异常及其处理决策,帮助区分自动化可解决与需人工介入的场景。 观察现象 可能原因 处理决定 标注整体偏移固定距离 CropBox 与 MediaBox 原点不一致 使用 page.cropbox_position 补偿偏移量 旋转后文字坐标正常但图片错位 图片使用了独立 CTM 变换矩阵 改用 get_image_rects() 而非 get_image_bbox() 部分页面旋转正确,部分错乱 文档混合了不同旋转角度的页面 逐页检查 page.rotation ,禁用全局缓存矩阵 变换后坐标超出页面边界 原始内容本身被裁切或存在负坐标 标记为异常页,转人工审查 0度页面无需变换但仍有偏差 第三方工具写入的隐藏旋转元数据 强制调用 remove_rotation() 重置后再处理 注意:本表针对坐标变换逻辑,不涵盖 OCR 精度或字体编码问题。当遇到复杂排版(如多栏混排+局部旋转图表)时,自动校验可能失效,此时应暂停流水线,由人工判断是否继续。 参考资料与下一步 PyMuPDF Page.rotation_matrix 官方文档 PyMuPDF Coordinates 概念说明 将这份检查清单用于文档翻译 先把页面顺序、来源位置与文本完整性检查清楚,再把同一份脱敏样本交给翻译流程。译后仍要检查标题、正文和图注是否正确对应,不能只看文字是否通顺。 OmniDoc(产品入口) 提供多格式文档翻译入口,官网介绍了 OCR、术语表与双语对照功能。对希望直接从文件开始处理的团队,这是一条值得用实际样本评估的路径。请使用有权上传的脱敏文件,并用本篇的检查项复核结果;这里不声称 OmniDoc 使用了本文的开源库或内部算法,也不以示例代替产品性能测试。 - 1 -
更新于 2026-10-08