"""端到端验证:process_image 输出 vs 实际写入 docx 的图片字节。 目的: 用户反馈"从文档复制出来的图底部有 1/4 空白",但独立测试 process_image 输出已是紧致裁剪。本脚本走完整 sign_word 链路,把生成的 docx 解开来看 word/media/ 下真实的图片字节,对比 process_image 的输出,定位空白来源: - 若字节完全一致 → 空白来自 Word 段落渲染(行高/对齐),不是图片本身 - 若字节不一致 → 我们的链路在某个环节给图片加了 padding,需进一步定位 使用: python image_test/e2e_test.py """ import io import sys import zipfile from pathlib import Path BASE_DIR = Path(__file__).resolve().parent PROJECT_ROOT = BASE_DIR.parent INPUT_IMAGE = BASE_DIR / "input" / "test1.jpg" OUT_DIR = BASE_DIR / "output" / "e2e" DOCX_OUT = OUT_DIR / "signed.docx" def _ensure_project_on_path(): if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) def _make_test_docx(path): """构造一份带 SET USER_test 域代码的 docx,正文段落使用偏大行高。 段落行高 2cm(lineRule=exact),模拟用户文档里的固定行高场景。 """ from docx import Document from docx.shared import Cm, Pt from docx.enum.text import WD_LINE_SPACING from lib import field_codes doc = Document() p = doc.add_paragraph() p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY p.paragraph_format.line_spacing = Cm(2.0) p.add_run("正文前 ") runs = field_codes.build_set_field_runs("test") for r in runs: p._element.append(r) p.add_run(" 正文中 ") p.add_run("正文后") doc.save(str(path)) def _image_stats(data): from PIL import Image, ImageChops img = Image.open(io.BytesIO(data)) if img.mode != 'RGBA': img = img.convert('RGBA') r, g, b, a = img.split() luma = Image.merge('RGB', (r, g, b)).convert('L') alpha_mask = a.point(lambda p: 255 if p > 30 else 0) luma_mask = luma.point(lambda p: 255 if p < 210 else 0) combined = ImageChops.darker(alpha_mask, luma_mask) total = img.size[0] * img.size[1] or 1 return { 'size': img.size, 'mode': img.mode, 'content_pct': 100.0 * sum(combined.histogram()[1:]) / total, 'alpha_pct': 100.0 * sum(alpha_mask.histogram()[1:]) / total, 'bbox': combined.getbbox(), 'sha256': __import__('hashlib').sha256(data).hexdigest()[:16], } def main(): _ensure_project_on_path() if not INPUT_IMAGE.exists(): print(f"测试图片不存在: {INPUT_IMAGE}") sys.exit(1) OUT_DIR.mkdir(parents=True, exist_ok=True) src_docx = OUT_DIR / "input.docx" _make_test_docx(src_docx) with open(src_docx, 'rb') as f: docx_data = f.read() with open(INPUT_IMAGE, 'rb') as f: image_bytes = f.read() from lib.image_processor import process_image from lib.word_signer import sign_word processed = process_image( image_bytes, is_signature=True, noise_level=0, rotate_min=0, rotate_max=0, log_source='e2e测试', ) params = { 'docx_name': 'input.docx', 'match_mode': 'upload', 'stamps': [{ 'marker': 'USER_test', 'image_base64': __import__('base64').b64encode(image_bytes).decode(), 'image_filename': 'test1.jpg', 'is_signature': True, }], 'use_global_height': True, 'height': 2.5, } result, err = sign_word(docx_data, params, log_source='e2e测试', trace_id='e2e') if err: print(f"sign_word 失败: {err}") sys.exit(1) with open(DOCX_OUT, 'wb') as f: f.write(result['data']) print(f"已生成: {DOCX_OUT}") print(f"warnings: {result.get('warnings', [])}") print(f"success_count: {result.get('success_count', 0)}") print() print("=== process_image 直出 ===") s1 = _image_stats(processed) print(f" 尺寸={s1['size']} 内容={s1['content_pct']:.1f}% " f"alpha={s1['alpha_pct']:.1f}% bbox={s1['bbox']} sha={s1['sha256']}") print() print("=== docx 内 word/media/ 实际图片 ===") with zipfile.ZipFile(DOCX_OUT, 'r') as z: medias = sorted(n for n in z.namelist() if n.startswith('word/media/')) if not medias: print(" 未找到任何图片!") return for name in medias: data = z.read(name) s2 = _image_stats(data) same = "一致" if data == processed else "不一致" print(f" [{name}] 尺寸={s2['size']} 内容={s2['content_pct']:.1f}% " f"alpha={s2['alpha_pct']:.1f}% bbox={s2['bbox']} sha={s2['sha256']} 字节={same}") print(f" 原始 {len(processed)} bytes vs docx {len(data)} bytes") if __name__ == '__main__': main()