151 lines
4.8 KiB
Python
151 lines
4.8 KiB
Python
"""端到端验证: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()
|