Python转换PDF中AutoCAD SHX文本为可搜索文本的坐标定位问题
问题背景
包含AutoCAD SHX Text的PDF无法直接进行文本搜索,需求是将PDF内所有AutoCAD SHX Text转换为可检索的普通文本,实现PDF全文搜索功能。参考现有注释转文本的实现方案落地时,存在两个核心问题:
- 无法获取文本精准位置,转换后的文本不能和原注释位置完全重合
- 生成的文本相对原文本偏转90度
目前已经实现读取PDF全页面内容、合并生成新PDF的基础功能,现有实现代码如下:
import io,os import PyPDF2 from PyPDF2 import PdfFileWriter, PdfFileReader from reportlab.pdfgen import canvas from reportlab.lib import colors from reportlab.lib.pagesizes import A4, landscape from datetime import date, time, datetime,timedelta def convert_annot(pdf_name): pdf = PdfFileReader(open(pdf_name, "rb")) numPages = pdf.getNumPages() output = PdfFileWriter() # 遍历所有页面 for i in range(0, numPages): page = pdf.getPage(i) objs=[] try : for annot in page['/Annots']: obj=annot.getObject() if 'AutoCAD SHX Text' in obj.values(): print (obj['/Contents'],obj['/Rect']) objs.append(obj) page_size=pdf.getPage(0).mediaBox.upperRight w, h =page_size packet = io.BytesIO() c = canvas.Canvas(packet,pagesize=landscape(A4)) c.setFont('Helvetica-Bold',12) c.setFillColor(colors.red) for obj in objs: if '/Contents' in obj: xy=tuple(obj['/Rect']) llx,lly,urx,ury=xy # 左下角X、左下角Y、右上角X、右上角Y text=obj['/Contents'] x1=int(urx) y1=int(lly) c.drawString(x1,y1,text) c.save() # 缓冲区指针重置到起始位置 packet.seek(0) new_pdf = PdfFileReader(packet) page=None new_pdf_file_name=None # 读取原有PDF页面合并新生成的文本层 page = pdf.pages[i] page.merge_page(new_pdf.pages[0]) output.add_page(page) except : page_size=pdf.getPage(0).mediaBox.upperRight w, h =page_size packet = io.BytesIO() c = canvas.Canvas(packet,pagesize=landscape(A4)) c.setFont('Helvetica-Bold',12) c.setFillColor(colors.red) for obj in objs: if '/Contents' in obj: xy=tuple(obj['/Rect']) llx,lly,urx,ury=xy text=obj['/Contents'] x1=int(urx) y1=int(lly) c.drawString(x1,y1,text) c.save() packet.seek(0) new_pdf = PdfFileReader(packet) page=None new_pdf_file_name=None page = pdf.pages[i] page.merge_page(new_pdf.pages[0]) output.add_page(page) # 输出最终PDF new_pdf_file_name=os.path.splitext(pdf_name)[0]+".annot.pdf" outputStream = open(new_pdf_file_name, "wb") output.write(outputStream) outputStream.close() if __name__ == '__main__': pdf_name='samplePdf.pdf' convert_annot(pdf_name)
效果对比
- 原始样例PDF:带不可选中、不可搜索的AutoCAD SHX Text注释

- 当前代码输出效果:红色的NOTES文本没有和原黑色AutoCAD SHX Text的NOTES位置完全重合

- 预期实现效果:转换后的可搜索文本和原注释位置完全对齐

解决方案
现有代码的两个问题不需要额外引入pdfminer就能解决,核心是三个逻辑错误:
- 页面尺寸硬编码为
landscape(A4),没有和原PDF页尺寸保持一致,这是位置偏移的核心原因 - 坐标取值错误:现有逻辑取注释矩形的右上角X、左下角Y作为文本绘制起点,正常文本绘制起点应该是矩形的左下角坐标
- 没有读取注释自带的旋转参数:AutoCAD导出的SHX文本注释会把旋转角度存在
/Rotation字段里,直接绘制自然会出现90度偏转
另外现有代码中try和except块的绘制逻辑完全重复,属于无效冗余,可以直接删除重复分支,只需要在访问annot字段前加存在性判断即可。
修正步骤
- 第一步:删除硬编码的A4页面尺寸,创建reportlab画布时直接使用原页面的实际宽高,保证坐标系完全对齐
- 第二步:修正文本绘制坐标,统一取注释矩形的左下角(llx, lly)作为绘制基准点,不要用右上角urx坐标
- 第三步:读取每个注释对象的
/Rotation参数,绘制文本前调用canvas的状态保存、平移、旋转方法做对应角度变换,绘制完成后恢复画布状态避免影响其他文本 - 第四步:不要固定写死12号字体,通过注释矩形的高度(ury - lly)动态计算适配的字体大小,避免文本大小和原注释不匹配
- 第五步:正式使用时把文本填充色透明度设为0,绘制的文本会完全透明,不影响原PDF显示效果,同时支持文本搜索选中,和原生可搜索PDF体验完全一致
修正后核心代码片段
替换原来遍历objs绘制文本的部分即可:
# 直接取当前页的实际尺寸,不要硬编码A4,也不要固定取第0页尺寸 page_size = page.mediabox.upperRight w, h = float(page_size[0]), float(page_size[1]) packet = io.BytesIO() c = canvas.Canvas(packet, pagesize=(w, h)) for obj in objs: if '/Contents' not in obj: continue llx, lly, urx, ury = [float(v) for v in obj['/Rect']] text = obj['/Contents'] # 读取旋转角度,默认0度 rotation = int(obj.get('/Rotation', 0)) # 计算适配的字体大小,取矩形高度的0.8倍避免溢出 font_size = (ury - lly) * 0.8 c.setFont('Helvetica-Bold', font_size) # 测试阶段用半透红色,正式使用把alpha改成0实现隐形文本 c.setFillColorRGB(1, 0, 0, alpha=0.8) # 保存画布状态,旋转后恢复避免影响其他文本绘制 c.saveState() # 平移到文本绘制起点 c.translate(llx, lly) # 按注释角度旋转,PDF坐标系y轴向上,旋转方向和常规图像坐标系相反 c.rotate(rotation) # 已经平移到基准点,坐标填(0,0)即可 c.drawString(0, 0, text) c.restoreState()
如果确实需要用pdfminer做二次坐标校验,可以按页面维度提前提取当前页所有文本块的坐标和内容,和注释的/Contents做文本匹配,取重叠度最高的文本块坐标作为绘制基准即可,不需要在遍历注释的循环里重复初始化pdfminer解析器,单页只需要解析一次。
内容的提问来源于stack exchange,提问作者Ejardy
相关产品推荐
相关产品推荐

