test_api.py 26 KB

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