test_api.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. PDF Converter API 测试脚本
  5. 测试新增的投资类型:
  6. - fsApproval: 可研批复
  7. - fsReview: 可研评审
  8. - pdApproval: 初设批复
  9. - safetyFsApproval: 安评可研批复
  10. 以及现有类型:
  11. - settlementReport: 结算报告
  12. - designReview: 初设评审
  13. """
  14. import os
  15. import sys
  16. import json
  17. import time
  18. import base64
  19. import requests
  20. from pathlib import Path
  21. from typing import Optional, Dict, Any, List
  22. # API 配置(默认本机 4214 端口;可通过环境变量 PDF_CONVERTER_API_URL 覆盖)
  23. API_BASE_URL = os.getenv("PDF_CONVERTER_API_URL", "http://localhost:4214")
  24. # 测试文件配置
  25. TEST_DIR = Path("/root/test/test")
  26. # 测试用例:文件名 -> (文档类型, 是否去水印, 是否只保留表格附件)
  27. # 格式:
  28. # "文件名": ("类型", 去水印, 只保留表格) - 完整格式
  29. # "文件名": ("类型", 去水印) - 兼容格式,只保留表格默认True
  30. # "文件名": "类型" - 旧格式,去水印False,只保留表格True
  31. TEST_CASES = {
  32. # 新增投资类型
  33. "鄂电司发展〔2024〕124号 国网湖北省电力有限公司关于襄阳连云220千伏输变电工程可行性研究报告的批复.pdf": ("safetyFsApproval", True,False), # 需要去水印 + 只保留表格附件
  34. "2-(可研批复)晋电发展〔2017〕831号+国网山西省电力公司关于临汾古县、晋城周村220kV输变电等工程可行性研究报告的批复.pdf.pdf": "fsApproval",
  35. "1-(可研评审)晋电经研规划〔2017〕187号(盖章)国网山西经研院关于山西晋城周村220kV输变电工程可行性研究报告的评审意见.pdf": "fsReview",
  36. "5-(初设批复)晋电建设〔2019〕566号 国网山西省电力公司关于晋城周村220kV输变电工程初步设计的批复 .pdf": "pdApproval",
  37. # 现有类型
  38. "9-(结算报告)山西晋城周村220kV输变电工程结算审计报告.pdf": "settlementReport",
  39. "4-(初设评审)中电联电力建设技术经济咨询中心技经〔2019〕201号关于山西周村220kV输变电工程初步设计的评审意见.pdf": "designReview",
  40. # 决算报告
  41. "10-(决算报告)盖章页-山西晋城周村220kV输变电工程竣工决算审核报告(中瑞诚鉴字(2021)第002040号).pdf": "finalAccount",
  42. }
  43. def print_header(title: str):
  44. """打印标题"""
  45. print("\n" + "=" * 60)
  46. print(f" {title}")
  47. print("=" * 60)
  48. def print_result(success: bool, message: str):
  49. """打印结果"""
  50. status = "✅ 成功" if success else "❌ 失败"
  51. print(f" {status}: {message}")
  52. def check_health() -> bool:
  53. """检查 API 健康状态"""
  54. print_header("检查 API 健康状态")
  55. try:
  56. response = requests.get(f"{API_BASE_URL}/health", timeout=10)
  57. if response.status_code == 200:
  58. print_result(True, f"API 正常运行 - {response.json()}")
  59. return True
  60. else:
  61. print_result(False, f"状态码: {response.status_code}")
  62. return False
  63. except requests.exceptions.RequestException as e:
  64. print_result(False, f"连接失败: {e}")
  65. return False
  66. def upload_file(file_path: Path, document_type: str, remove_watermark: bool = False, table_only: bool = True) -> Optional[str]:
  67. """上传文件并获取任务 ID
  68. Args:
  69. file_path: 文件路径
  70. document_type: 文档类型
  71. remove_watermark: 是否去水印
  72. table_only: 是否只保留表格附件
  73. """
  74. print(f"\n 📤 上传文件: {file_path.name}")
  75. print(f" 类型: {document_type}")
  76. if remove_watermark:
  77. print(f" 去水印: 是")
  78. if table_only:
  79. print(f" 只保留表格: 是")
  80. try:
  81. with open(file_path, "rb") as f:
  82. files = {"file": (file_path.name, f, "application/pdf")}
  83. # 使用 data 发送表单参数,参数名是 type(不是 document_type)
  84. data = {"type": document_type}
  85. # 添加去水印参数
  86. if remove_watermark:
  87. data["remove_watermark"] = "true"
  88. data["watermark_light_threshold"] = "200"
  89. data["watermark_saturation_threshold"] = "30"
  90. # 添加只保留表格参数
  91. data["table_only"] = "true" if table_only else "false"
  92. response = requests.post(
  93. f"{API_BASE_URL}/convert",
  94. files=files,
  95. data=data,
  96. timeout=60
  97. )
  98. if response.status_code == 200:
  99. result = response.json()
  100. task_id = result.get("task_id")
  101. print(f" 任务 ID: {task_id}")
  102. return task_id
  103. else:
  104. print_result(False, f"上传失败: {response.status_code} - {response.text}")
  105. return None
  106. except Exception as e:
  107. print_result(False, f"上传异常: {e}")
  108. return None
  109. def poll_task_status(task_id: str, max_wait: int = 300) -> Optional[Dict[str, Any]]:
  110. """轮询任务状态"""
  111. print(f" ⏳ 等待任务完成...")
  112. start_time = time.time()
  113. poll_interval = 5 # 轮询间隔(秒)
  114. while time.time() - start_time < max_wait:
  115. try:
  116. response = requests.get(f"{API_BASE_URL}/task/{task_id}", timeout=10)
  117. if response.status_code == 200:
  118. result = response.json()
  119. status = result.get("status")
  120. if status == "completed":
  121. elapsed = time.time() - start_time
  122. print(f" 完成! 耗时: {elapsed:.1f}s")
  123. return result
  124. elif status == "failed":
  125. error = result.get("error", "未知错误")
  126. print_result(False, f"任务失败: {error}")
  127. return None
  128. else:
  129. # 仍在处理中
  130. elapsed = time.time() - start_time
  131. print(f" 处理中... ({elapsed:.0f}s)", end="\r")
  132. else:
  133. print_result(False, f"查询状态失败: {response.status_code}")
  134. return None
  135. except Exception as e:
  136. print_result(False, f"查询异常: {e}")
  137. return None
  138. time.sleep(poll_interval)
  139. print_result(False, f"超时: 超过 {max_wait} 秒")
  140. return None
  141. def get_json_result(task_id: str) -> Optional[Dict[str, Any]]:
  142. """获取 JSON 结果"""
  143. try:
  144. response = requests.get(f"{API_BASE_URL}/task/{task_id}/json", timeout=30)
  145. if response.status_code == 200:
  146. return response.json()
  147. else:
  148. print_result(False, f"获取 JSON 失败: {response.status_code}")
  149. return None
  150. except Exception as e:
  151. print_result(False, f"获取 JSON 异常: {e}")
  152. return None
  153. def validate_result(result: Dict[str, Any], expected_type: str) -> bool:
  154. """验证结果"""
  155. document_type = result.get("document_type")
  156. data = result.get("data")
  157. # 检查文档类型
  158. if document_type != expected_type:
  159. print_result(False, f"文档类型不匹配: 期望 {expected_type}, 实际 {document_type}")
  160. return False
  161. # 检查数据是否为空
  162. if not data:
  163. print_result(False, "数据为空")
  164. return False
  165. # 对于投资类型,检查嵌套结构
  166. if expected_type in ["fsApproval", "fsReview", "pdApproval", "safetyFsApproval"]:
  167. # 检查是否是新格式(包含 projectInfo)
  168. project_info = None
  169. if isinstance(data, dict) and "data" in data:
  170. # 新格式:{"projectInfo": {...}, "data": [...]}
  171. project_info = data.get("projectInfo")
  172. data = data["data"]
  173. if project_info:
  174. print(f"\n 📋 项目信息:")
  175. print(f" 工程名称: {project_info.get('projectName', '')}")
  176. print(f" 项目单位: {project_info.get('projectUnit', '')}")
  177. print(f" 设计单位: {project_info.get('designUnit', '')}")
  178. # 验证数据格式
  179. if not isinstance(data, list):
  180. print_result(False, f"数据格式错误: 期望 list, 实际 {type(data).__name__}")
  181. return False
  182. if len(data) == 0:
  183. print_result(False, "投资数据列表为空")
  184. return False
  185. # 检查第一项的结构
  186. first_item = data[0]
  187. required_fields = ["name", "Level", "staticInvestment", "dynamicInvestment", "items"]
  188. missing_fields = [f for f in required_fields if f not in first_item]
  189. if missing_fields:
  190. print_result(False, f"缺少字段: {missing_fields}")
  191. return False
  192. print_result(True, f"解析到 {len(data)} 个大类")
  193. # 打印摘要
  194. for item in data:
  195. name = item.get("name", "")
  196. static = item.get("staticInvestment", 0)
  197. dynamic = item.get("dynamicInvestment", 0)
  198. sub_items = len(item.get("items", []))
  199. print(f" - {name}: 静态={static}, 动态={dynamic}, 子项={sub_items}")
  200. # 对于结算报告
  201. elif expected_type == "settlementReport":
  202. if isinstance(data, list):
  203. print_result(True, f"解析到 {len(data)} 条记录")
  204. else:
  205. print_result(True, f"解析完成")
  206. # 对于初设评审
  207. elif expected_type == "designReview":
  208. if isinstance(data, list):
  209. print_result(True, f"解析到 {len(data)} 条记录")
  210. else:
  211. print_result(True, f"解析完成")
  212. return True
  213. def test_single_file(file_path: Path, document_type: str, remove_watermark: bool = False, table_only: bool = True) -> bool:
  214. """测试单个文件
  215. Args:
  216. file_path: 文件路径
  217. document_type: 文档类型
  218. remove_watermark: 是否去水印
  219. table_only: 是否只保留表格附件
  220. """
  221. print_header(f"测试: {document_type}")
  222. print(f" 文件: {file_path.name}")
  223. if remove_watermark:
  224. print(f" 去水印: 是")
  225. if table_only:
  226. print(f" 只保留表格: 是")
  227. # 1. 上传文件
  228. task_id = upload_file(file_path, document_type, remove_watermark, table_only)
  229. if not task_id:
  230. return False
  231. # 2. 等待任务完成
  232. task_result = poll_task_status(task_id)
  233. if not task_result:
  234. return False
  235. # 3. 获取 JSON 结果
  236. json_result = get_json_result(task_id)
  237. if not json_result:
  238. return False
  239. # 4. 验证结果
  240. is_valid = validate_result(json_result, document_type)
  241. # 5. 保存结果到文件
  242. output_dir = Path(__file__).parent / "test_results"
  243. output_dir.mkdir(exist_ok=True)
  244. output_file = output_dir / f"{document_type}_result.json"
  245. with open(output_file, "w", encoding="utf-8") as f:
  246. json.dump(json_result, f, ensure_ascii=False, indent=2)
  247. print(f" 💾 结果已保存: {output_file}")
  248. return is_valid
  249. def run_all_tests():
  250. """运行所有测试"""
  251. print_header("PDF Converter API 测试")
  252. print(f" API 地址: {API_BASE_URL}")
  253. print(f" 测试目录: {TEST_DIR}")
  254. # 检查测试目录
  255. if not TEST_DIR.exists():
  256. print_result(False, f"测试目录不存在: {TEST_DIR}")
  257. return
  258. # 检查 API 健康状态
  259. if not check_health():
  260. print("\n❌ API 不可用,终止测试")
  261. return
  262. # 统计结果
  263. total = 0
  264. passed = 0
  265. failed = 0
  266. skipped = 0
  267. # 运行每个测试用例
  268. for filename, config in TEST_CASES.items():
  269. # 解析配置格式
  270. if isinstance(config, tuple):
  271. if len(config) >= 3:
  272. document_type, remove_watermark, table_only = config[:3]
  273. elif len(config) == 2:
  274. document_type, remove_watermark = config
  275. table_only = True # 默认只保留表格
  276. else:
  277. document_type = config[0]
  278. remove_watermark = False
  279. table_only = True
  280. else:
  281. document_type = config
  282. remove_watermark = False
  283. table_only = True
  284. file_path = TEST_DIR / filename
  285. if not file_path.exists():
  286. print_header(f"跳过: {document_type}")
  287. print_result(False, f"文件不存在: {filename}")
  288. skipped += 1
  289. continue
  290. total += 1
  291. try:
  292. if test_single_file(file_path, document_type, remove_watermark, table_only):
  293. passed += 1
  294. else:
  295. failed += 1
  296. except Exception as e:
  297. print_result(False, f"测试异常: {e}")
  298. failed += 1
  299. # 打印总结
  300. print_header("测试总结")
  301. print(f" 总计: {total}")
  302. print(f" ✅ 通过: {passed}")
  303. print(f" ❌ 失败: {failed}")
  304. print(f" ⏭️ 跳过: {skipped}")
  305. if failed == 0 and skipped == 0:
  306. print("\n🎉 所有测试通过!")
  307. elif failed > 0:
  308. print(f"\n⚠️ 有 {failed} 个测试失败")
  309. def test_single(document_type: str):
  310. """测试单个类型"""
  311. print_header(f"单项测试: {document_type}")
  312. # 检查 API
  313. if not check_health():
  314. print("\n❌ API 不可用")
  315. return
  316. # 查找对应的文件
  317. for filename, config in TEST_CASES.items():
  318. # 解析配置格式
  319. if isinstance(config, tuple):
  320. if len(config) >= 3:
  321. dtype, remove_watermark, table_only = config[:3]
  322. elif len(config) == 2:
  323. dtype, remove_watermark = config
  324. table_only = True
  325. else:
  326. dtype = config[0]
  327. remove_watermark = False
  328. table_only = True
  329. else:
  330. dtype = config
  331. remove_watermark = False
  332. table_only = True
  333. if dtype == document_type:
  334. file_path = TEST_DIR / filename
  335. if file_path.exists():
  336. test_single_file(file_path, document_type, remove_watermark, table_only)
  337. return
  338. else:
  339. print_result(False, f"文件不存在: {filename}")
  340. return
  341. print_result(False, f"未找到类型 {document_type} 的测试文件")
  342. def test_ocr(
  343. image_path: Optional[str] = None,
  344. remove_watermark: bool = False,
  345. light_threshold: int = 200,
  346. saturation_threshold: int = 30,
  347. crop_header_footer: bool = False,
  348. header_ratio: float = 0.05,
  349. footer_ratio: float = 0.05,
  350. auto_detect_header_footer: bool = False
  351. ) -> bool:
  352. """
  353. 测试 OCR 接口
  354. Args:
  355. image_path: 图片路径或包含base64数据的txt文件路径,默认使用 test/image.png
  356. 支持格式:
  357. - 图片文件:.png, .jpg, .jpeg
  358. - txt文件:包含base64编码的图片数据(可带data:image/xxx;base64,前缀)
  359. remove_watermark: 是否去除水印
  360. light_threshold: 水印亮度阈值(0-255),默认200
  361. saturation_threshold: 水印饱和度阈值(0-255),默认30
  362. crop_header_footer: 是否裁剪页眉页脚
  363. header_ratio: 页眉裁剪比例(0-1),默认0.05
  364. footer_ratio: 页脚裁剪比例(0-1),默认0.05
  365. auto_detect_header_footer: 是否自动检测页眉页脚边界
  366. Returns:
  367. 是否测试成功
  368. """
  369. print_header("测试 OCR 接口")
  370. # 检查 API
  371. if not check_health():
  372. print("\n❌ API 不可用")
  373. return False
  374. # 确定图片路径
  375. if image_path is None:
  376. image_path = TEST_DIR / "image.png"
  377. else:
  378. image_path = Path(image_path)
  379. print(f" 📷 文件路径: {image_path}")
  380. if not image_path.exists():
  381. print_result(False, f"文件不存在: {image_path}")
  382. return False
  383. suffix = image_path.suffix.lower()
  384. # 判断是 txt 文件还是图片文件
  385. if suffix == ".txt":
  386. # 从 txt 文件读取 base64 数据
  387. print(f" 📄 文件类型: txt (base64 数据)")
  388. try:
  389. with open(image_path, "r", encoding="utf-8") as f:
  390. image_base64 = f.read().strip()
  391. # 解析 data URI,提取格式和 base64 数据
  392. if image_base64.startswith("data:"):
  393. # 格式: data:image/png;base64,xxxxx
  394. if "," in image_base64:
  395. header, image_base64 = image_base64.split(",", 1)
  396. # 从 header 中提取图片格式
  397. if "image/png" in header:
  398. image_format = "png"
  399. elif "image/jpeg" in header or "image/jpg" in header:
  400. image_format = "jpeg"
  401. else:
  402. image_format = "png" # 默认
  403. print(f" 🖼️ 图片格式 (从data URI解析): {image_format}")
  404. else:
  405. image_format = "png"
  406. print(f" 🖼️ 图片格式 (默认): {image_format}")
  407. else:
  408. image_format = "png"
  409. print(f" 🖼️ 图片格式 (默认): {image_format}")
  410. print(f" 🔤 Base64长度: {len(image_base64)} 字符")
  411. except Exception as e:
  412. print_result(False, f"读取txt文件失败: {e}")
  413. return False
  414. else:
  415. # 读取图片文件并转为 base64
  416. print(f" 📄 文件类型: 图片文件")
  417. try:
  418. with open(image_path, "rb") as f:
  419. image_data = f.read()
  420. image_base64 = base64.b64encode(image_data).decode("utf-8")
  421. print(f" 📦 图片大小: {len(image_data)} bytes")
  422. print(f" 🔤 Base64长度: {len(image_base64)} 字符")
  423. except Exception as e:
  424. print_result(False, f"读取图片失败: {e}")
  425. return False
  426. # 确定图片格式
  427. format_map = {".png": "png", ".jpg": "jpeg", ".jpeg": "jpeg"}
  428. image_format = format_map.get(suffix, "png")
  429. print(f" 🖼️ 图片格式: {image_format}")
  430. # 调用 OCR 接口
  431. print(f"\n 📤 调用 OCR 接口...")
  432. # 构建请求参数
  433. request_data = {
  434. "image_base64": image_base64,
  435. "image_format": image_format
  436. }
  437. if crop_header_footer:
  438. request_data["crop_header_footer"] = True
  439. if auto_detect_header_footer:
  440. request_data["auto_detect_header_footer"] = True
  441. print(f" ✂️ 裁剪页眉页脚: 自动检测模式")
  442. else:
  443. request_data["header_ratio"] = header_ratio
  444. request_data["footer_ratio"] = footer_ratio
  445. print(f" ✂️ 裁剪页眉页脚: 是 (顶部={header_ratio*100:.0f}%, 底部={footer_ratio*100:.0f}%)")
  446. if remove_watermark:
  447. request_data["remove_watermark"] = True
  448. request_data["watermark_light_threshold"] = light_threshold
  449. request_data["watermark_saturation_threshold"] = saturation_threshold
  450. print(f" 🔧 去水印: 是 (亮度阈值={light_threshold}, 饱和度阈值={saturation_threshold})")
  451. try:
  452. start_time = time.time()
  453. response = requests.post(
  454. f"{API_BASE_URL}/ocr",
  455. json=request_data,
  456. timeout=120
  457. )
  458. elapsed = time.time() - start_time
  459. if response.status_code == 200:
  460. result = response.json()
  461. print_result(True, f"OCR 识别成功 (耗时: {elapsed:.2f}s)")
  462. # 显示识别结果(支持两种返回格式)
  463. # 格式1: {"texts": [...], "gpu_info": {...}}
  464. # 格式2: {"code": 0, "data": {"texts": [...]}, "gpu_info": {...}}
  465. if "data" in result and isinstance(result.get("data"), dict):
  466. texts: List[str] = result.get("data", {}).get("texts", [])
  467. else:
  468. texts: List[str] = result.get("texts", [])
  469. gpu_info = result.get("gpu_info", {})
  470. print(f"\n 📝 识别结果 ({len(texts)} 个文本块):")
  471. for i, text in enumerate(texts[:10]): # 最多显示前10个
  472. # 截断长文本
  473. display_text = text[:50] + "..." if len(text) > 50 else text
  474. print(f" [{i+1}] {display_text}")
  475. if len(texts) > 10:
  476. print(f" ... 还有 {len(texts) - 10} 个文本块")
  477. # 显示 GPU 信息
  478. if gpu_info:
  479. print(f"\n 💻 GPU 监控信息:")
  480. gpu_util = gpu_info.get('gpu_utilization', gpu_info.get('gpu_util_avg', 'N/A'))
  481. if isinstance(gpu_util, float):
  482. gpu_util = f"{gpu_util:.1f}"
  483. print(f" GPU利用率: {gpu_util}%")
  484. mem_used = gpu_info.get('gpu_memory_used_max', gpu_info.get('memory_used_max', 'N/A'))
  485. if isinstance(mem_used, (int, float)):
  486. mem_used = f"{mem_used / (1024**2):.0f}" # 转为 MB
  487. print(f" 显存使用峰值: {mem_used} MB")
  488. gpu_name = gpu_info.get('gpu_name', 'N/A')
  489. print(f" GPU型号: {gpu_name}")
  490. # 保存完整结果
  491. output_dir = Path(__file__).parent / "test_results"
  492. output_dir.mkdir(exist_ok=True)
  493. output_file = output_dir / "ocr_result.json"
  494. with open(output_file, "w", encoding="utf-8") as f:
  495. json.dump(result, f, ensure_ascii=False, indent=2)
  496. print(f"\n 💾 结果已保存: {output_file}")
  497. return True
  498. else:
  499. print_result(False, f"OCR 失败: {response.status_code} - {response.text}")
  500. return False
  501. except requests.exceptions.Timeout:
  502. print_result(False, "OCR 请求超时")
  503. return False
  504. except Exception as e:
  505. print_result(False, f"OCR 异常: {e}")
  506. return False
  507. if __name__ == "__main__":
  508. if len(sys.argv) > 1:
  509. # 测试指定类型
  510. doc_type = sys.argv[1]
  511. if doc_type in ["--help", "-h"]:
  512. print("用法:")
  513. print(" python test_api.py # 运行所有测试")
  514. print(" python test_api.py <type> # 测试指定类型")
  515. print(" python test_api.py ocr # 测试 OCR 接口")
  516. print(" python test_api.py ocr <image_path> # 测试 OCR(指定图片或txt)")
  517. print(" python test_api.py ocr <image_path> --nowm # 测试 OCR 并去水印")
  518. print(" python test_api.py ocr <image_path> --crop # 测试 OCR 并裁剪页眉页脚")
  519. print(" python test_api.py ocr <image_path> --nowm --crop # 同时去水印和裁剪")
  520. print("\n可用类型:")
  521. for dtype in set(TEST_CASES.values()):
  522. print(f" - {dtype}")
  523. print(" - ocr (OCR 图片识别)")
  524. print("\nOCR 去水印参数:")
  525. print(" --nowm 启用去水印")
  526. print(" --light=N 亮度阈值(0-255,默认200)")
  527. print(" --sat=N 饱和度阈值(0-255,默认30)")
  528. print("\nOCR 裁剪页眉页脚参数:")
  529. print(" --crop 启用裁剪页眉页脚(固定比例模式)")
  530. print(" --crop-auto 启用裁剪页眉页脚(自动检测模式)")
  531. print(" --header=N 页眉裁剪比例(0-1,默认0.05表示5%)")
  532. print(" --footer=N 页脚裁剪比例(0-1,默认0.05表示5%)")
  533. elif doc_type == "ocr":
  534. # 解析 OCR 参数
  535. image_path = None
  536. remove_watermark = False
  537. light_threshold = 200
  538. saturation_threshold = 30
  539. crop_header_footer = False
  540. header_ratio = 0.05
  541. footer_ratio = 0.05
  542. auto_detect_header_footer = False
  543. for arg in sys.argv[2:]:
  544. if arg == "--nowm":
  545. remove_watermark = True
  546. elif arg == "--crop":
  547. crop_header_footer = True
  548. elif arg == "--crop-auto":
  549. crop_header_footer = True
  550. auto_detect_header_footer = True
  551. elif arg.startswith("--light="):
  552. try:
  553. light_threshold = int(arg.split("=")[1])
  554. except ValueError:
  555. print(f"警告: 无效的亮度阈值 {arg},使用默认值 200")
  556. elif arg.startswith("--sat="):
  557. try:
  558. saturation_threshold = int(arg.split("=")[1])
  559. except ValueError:
  560. print(f"警告: 无效的饱和度阈值 {arg},使用默认值 30")
  561. elif arg.startswith("--header="):
  562. try:
  563. header_ratio = float(arg.split("=")[1])
  564. except ValueError:
  565. print(f"警告: 无效的页眉比例 {arg},使用默认值 0.05")
  566. elif arg.startswith("--footer="):
  567. try:
  568. footer_ratio = float(arg.split("=")[1])
  569. except ValueError:
  570. print(f"警告: 无效的页脚比例 {arg},使用默认值 0.05")
  571. elif not arg.startswith("--"):
  572. image_path = arg
  573. test_ocr(
  574. image_path,
  575. remove_watermark,
  576. light_threshold,
  577. saturation_threshold,
  578. crop_header_footer,
  579. header_ratio,
  580. footer_ratio,
  581. auto_detect_header_footer
  582. )
  583. else:
  584. test_single(doc_type)
  585. else:
  586. # 运行所有测试
  587. run_all_tests()