first commit
BIN
image_test/__pycache__/process.cpython-39.pyc
Normal file
514
image_test/auto_size_test.py
Normal file
@@ -0,0 +1,514 @@
|
||||
"""验证 auto-size 和 relax_layout:
|
||||
|
||||
1. auto-size:在不同字号/段落行高的段落里插入签名(height_cm=None),
|
||||
检查最终插入尺寸是否符合预期。
|
||||
|
||||
2. relax_layout:当段落/表格行有固定高度约束(lineRule=exact / hRule=exact)
|
||||
小于签名需要的高度时,自动放宽约束;以及 relax_layout=False 时缩放到约束内。
|
||||
"""
|
||||
import io
|
||||
import sys
|
||||
import zipfile
|
||||
from lxml import etree as ET
|
||||
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" / "auto_size"
|
||||
|
||||
W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||
WP = 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'
|
||||
|
||||
|
||||
def _ensure_path():
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
def emu_to_cm(emu):
|
||||
return int(emu) / 360000.0
|
||||
|
||||
|
||||
def make_docx(path, scenarios):
|
||||
"""scenarios: [(label, font_half_pt, line_twips_or_None), ...]
|
||||
生成一份 docx,每个 scenario 一段(带 SET USER_xxx),字号和行高按参数设置。
|
||||
"""
|
||||
from docx import Document
|
||||
from docx.enum.text import WD_LINE_SPACING
|
||||
from lib import field_codes
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
doc = Document()
|
||||
for i, (label, font_half_pt, line_twips) in enumerate(scenarios):
|
||||
p = doc.add_paragraph()
|
||||
# 字号
|
||||
run = p.add_run(f"")
|
||||
rPr = run._element.get_or_add_rPr()
|
||||
sz = OxmlElement('w:sz')
|
||||
sz.set(qn('w:val'), str(font_half_pt))
|
||||
rPr.append(sz)
|
||||
sz_cs = OxmlElement('w:szCs')
|
||||
sz_cs.set(qn('w:val'), str(font_half_pt))
|
||||
rPr.append(sz_cs)
|
||||
|
||||
# 段落标记也设字号(pPr/rPr/sz)
|
||||
pPr = p._element.get_or_add_pPr()
|
||||
p_rPr = OxmlElement('w:rPr')
|
||||
p_sz = OxmlElement('w:sz')
|
||||
p_sz.set(qn('w:val'), str(font_half_pt))
|
||||
p_rPr.append(p_sz)
|
||||
pPr.append(p_rPr)
|
||||
|
||||
# 行高
|
||||
if line_twips:
|
||||
spacing = OxmlElement('w:spacing')
|
||||
spacing.set(qn('w:line'), str(line_twips))
|
||||
spacing.set(qn('w:lineRule'), 'exact')
|
||||
pPr.append(spacing)
|
||||
|
||||
marker_name = f"u{i}"
|
||||
for r in field_codes.build_set_field_runs(marker_name):
|
||||
p._element.append(r)
|
||||
|
||||
doc.save(str(path))
|
||||
|
||||
|
||||
def make_table_docx(path, scenarios):
|
||||
"""scenarios: [(label, font_half_pt, row_height_twips_or_None, hRule, indent_twips_or_None)]
|
||||
生成一份 docx,每个 scenario 是一个独立的 1×1 表格,单元格内含 SET USER_txxx。
|
||||
row_height + hRule 控制行高;indent 控制单元格内段落左右缩进。
|
||||
"""
|
||||
from docx import Document
|
||||
from lib import field_codes
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
doc = Document()
|
||||
for i, (label, font_half_pt, row_h_twips, h_rule, indent_twips) in enumerate(scenarios):
|
||||
table = doc.add_table(rows=1, cols=1)
|
||||
cell = table.rows[0].cells[0]
|
||||
|
||||
# 行高
|
||||
if row_h_twips:
|
||||
trPr = table.rows[0]._tr.get_or_add_trPr()
|
||||
trHeight = OxmlElement('w:trHeight')
|
||||
trHeight.set(qn('w:val'), str(row_h_twips))
|
||||
trHeight.set(qn('w:hRule'), h_rule or 'atLeast')
|
||||
trPr.append(trHeight)
|
||||
|
||||
# 单元格宽度(固定 5cm 便于断言)
|
||||
tcPr = cell._tc.get_or_add_tcPr()
|
||||
tcW = OxmlElement('w:tcW')
|
||||
tcW.set(qn('w:w'), str(int(5 * 566.93)))
|
||||
tcW.set(qn('w:type'), 'dxa')
|
||||
tcPr.append(tcW)
|
||||
|
||||
p = cell.paragraphs[0]
|
||||
# 字号
|
||||
run = p.add_run(f"")
|
||||
rPr = run._element.get_or_add_rPr()
|
||||
sz = OxmlElement('w:sz')
|
||||
sz.set(qn('w:val'), str(font_half_pt))
|
||||
rPr.append(sz)
|
||||
|
||||
# 段落缩进
|
||||
if indent_twips:
|
||||
ppPr = p._element.get_or_add_pPr()
|
||||
ind = OxmlElement('w:ind')
|
||||
ind.set(qn('w:left'), str(indent_twips))
|
||||
ind.set(qn('w:right'), str(indent_twips))
|
||||
ppPr.append(ind)
|
||||
|
||||
marker_name = f"u0" if len(scenarios) == 1 else f"t{i}"
|
||||
for r in field_codes.build_set_field_runs(marker_name):
|
||||
p._element.append(r)
|
||||
|
||||
doc.add_paragraph() # 表格之间留空段
|
||||
|
||||
doc.save(str(path))
|
||||
|
||||
|
||||
def _sign(docx_path, height, relax_layout=True):
|
||||
"""调用 sign_word 签出 docx,返回 signed bytes。"""
|
||||
import base64
|
||||
from lib.word_signer import sign_word
|
||||
|
||||
with open(docx_path, 'rb') as f:
|
||||
docx_data = f.read()
|
||||
with open(INPUT_IMAGE, 'rb') as f:
|
||||
image_bytes = f.read()
|
||||
|
||||
stamps = [{'marker': 'USER_u0', 'image_base64': base64.b64encode(image_bytes).decode(),
|
||||
'image_filename': 'test1.jpg', 'is_signature': True}]
|
||||
params = {
|
||||
'docx_name': 'input.docx',
|
||||
'match_mode': 'upload',
|
||||
'stamps': stamps,
|
||||
'use_global_height': True,
|
||||
'height': height,
|
||||
'relax_layout': relax_layout,
|
||||
}
|
||||
result, err = sign_word(docx_data, params, log_source='auto测试', trace_id=f'relax_{int(relax_layout)}')
|
||||
if err:
|
||||
print(f"sign_word 失败: {err}")
|
||||
sys.exit(1)
|
||||
return result['data']
|
||||
|
||||
|
||||
def _read_xml(signed_bytes, member='word/document.xml'):
|
||||
with zipfile.ZipFile(io.BytesIO(signed_bytes)) as z:
|
||||
return z.read(member)
|
||||
|
||||
|
||||
def _ns(tag, ns):
|
||||
return f'{{{ns}}}{tag}'
|
||||
|
||||
|
||||
def test_auto_size(src_docx, scenarios):
|
||||
"""原 auto-size 测试:height=None,按字号/行高推算。"""
|
||||
import base64
|
||||
from lib.word_signer import sign_word
|
||||
|
||||
with open(src_docx, 'rb') as f:
|
||||
docx_data = f.read()
|
||||
with open(INPUT_IMAGE, 'rb') as f:
|
||||
image_bytes = f.read()
|
||||
|
||||
params = {
|
||||
'docx_name': 'input.docx', 'match_mode': 'upload',
|
||||
'stamps': [{'marker': f'USER_u{i}', 'image_base64': base64.b64encode(image_bytes).decode(),
|
||||
'image_filename': 'test1.jpg', 'is_signature': True}
|
||||
for i in range(len(scenarios))],
|
||||
'use_global_height': True, 'height': None,
|
||||
}
|
||||
result, err = sign_word(docx_data, params, log_source='auto测试', trace_id='auto')
|
||||
if err:
|
||||
print(f"sign_word 失败: {err}")
|
||||
sys.exit(1)
|
||||
|
||||
signed = OUT_DIR / "signed_auto.docx"
|
||||
with open(signed, 'wb') as f:
|
||||
f.write(result['data'])
|
||||
|
||||
root = ET.fromstring(_read_xml(result['data']))
|
||||
print(f"\n=== auto-size 测试 (height=None) ===\n")
|
||||
print(f"{'场景':<22} {'字号':<8} {'行高':<10} {'图片显示尺寸':<20} {'判定'}")
|
||||
print("-" * 80)
|
||||
|
||||
para_idx = 0
|
||||
pass_cnt = 0
|
||||
for p in root.iter(_ns('p', W)):
|
||||
inlines = list(p.iter(_ns('inline', WP)))
|
||||
if not inlines:
|
||||
continue
|
||||
if para_idx >= len(scenarios):
|
||||
break
|
||||
label, font_half, line_twips = scenarios[para_idx]
|
||||
font_pt = font_half / 2.0
|
||||
line_cm = (line_twips / 567.0) if line_twips else None
|
||||
|
||||
extent = inlines[0].find(_ns('extent', WP))
|
||||
cx = emu_to_cm(extent.get('cx'))
|
||||
cy = emu_to_cm(extent.get('cy'))
|
||||
|
||||
if line_cm:
|
||||
expected = round(line_cm * 0.95, 2)
|
||||
ok = abs(cy - expected) < 0.05
|
||||
verdict = f"{'✓' if ok else '✗'} ≈行高95%(期望{expected}cm)"
|
||||
else:
|
||||
expected = round(font_pt * 2 * 2.54 / 72, 2)
|
||||
ok = abs(cy - expected) < 0.05
|
||||
verdict = f"{'✓' if ok else '✗'} ≈2倍字号(期望{expected}cm)"
|
||||
if ok: pass_cnt += 1
|
||||
|
||||
print(f"{label:<22} {font_pt:<8.1f} {f'{line_cm:.2f}cm' if line_cm else '无':<10} "
|
||||
f"{cx:.2f}×{cy:.2f}cm{'':<10} {verdict}")
|
||||
para_idx += 1
|
||||
print(f"\nauto-size: {pass_cnt}/{len(scenarios)} 通过")
|
||||
|
||||
|
||||
def test_relax_paragraph():
|
||||
"""段落行高约束 + 用户指定 height 大于行高 → 应放宽到 height。"""
|
||||
scenarios = [
|
||||
# (label, line_twips, expected_relax_to_height_cm)
|
||||
("行高exact 0.5cm + 目标2cm(应放宽)", 283, 2.0),
|
||||
("行高exact 5cm + 目标2cm(无需放宽)", 2835, 2.0),
|
||||
]
|
||||
print(f"\n=== relax_layout 段落行高测试 (height=2cm) ===\n")
|
||||
print(f"{'场景':<40} {'lineRule':<10} {'line(twips)':<12} {'图片高':<10} {'判定'}")
|
||||
print("-" * 90)
|
||||
|
||||
pass_cnt = 0
|
||||
for label, line_twips, target_h in scenarios:
|
||||
src = OUT_DIR / f"para_{line_twips}.docx"
|
||||
make_docx(src, [(label, 21, line_twips)])
|
||||
signed = _sign(src, height=target_h, relax_layout=True)
|
||||
root = ET.fromstring(_read_xml(signed))
|
||||
|
||||
# 找到 SET 域所在段落的 pPr/spacing
|
||||
p_with_image = None
|
||||
for p in root.iter(_ns('p', W)):
|
||||
if list(p.iter(_ns('inline', WP))):
|
||||
p_with_image = p
|
||||
break
|
||||
|
||||
pPr = p_with_image.find(_ns('pPr', W)) if p_with_image is not None else None
|
||||
spacing = pPr.find(_ns('spacing', W)) if pPr is not None else None
|
||||
line_rule = spacing.get(_ns('lineRule', W)) if spacing is not None else None
|
||||
line_val = spacing.get(_ns('line', W)) if spacing is not None else None
|
||||
|
||||
extent = list(p_with_image.iter(_ns('extent', WP)))[0]
|
||||
img_h = emu_to_cm(extent.get('cy'))
|
||||
|
||||
line_cm_input = line_twips / 567.0
|
||||
if line_cm_input < target_h:
|
||||
expected_rule = 'atLeast'
|
||||
expected_line = int(target_h * 566.93 + 0.999)
|
||||
ok = (line_rule == expected_rule and abs(int(line_val) - expected_line) <= 1
|
||||
and abs(img_h - target_h) < 0.05)
|
||||
verdict = f"{'✓' if ok else '✗'} 期望 atLeast/{expected_line}"
|
||||
else:
|
||||
expected_rule = 'exact'
|
||||
ok = (line_rule == expected_rule and abs(img_h - target_h) < 0.05)
|
||||
verdict = f"{'✓' if ok else '✗'} 期望保持 exact"
|
||||
if ok: pass_cnt += 1
|
||||
|
||||
print(f"{label:<40} {line_rule or '无':<10} {line_val or '无':<12} "
|
||||
f"{img_h:.2f}cm {verdict}")
|
||||
print(f"\nrelax 段落行高: {pass_cnt}/{len(scenarios)} 通过")
|
||||
|
||||
|
||||
def test_relax_table_row():
|
||||
"""表格行固定高度 + 用户指定 height 大于行高 → 应放宽。"""
|
||||
scenarios = [
|
||||
# (label, row_h_twips, hRule, expected_action)
|
||||
("表格行 exact 1cm + 目标2cm", 567, 'exact', 'relax'),
|
||||
("表格行 atLeast 1cm + 目标2cm", 567, 'atLeast', 'keep'),
|
||||
]
|
||||
print(f"\n=== relax_layout 表格行高测试 (height=2cm) ===\n")
|
||||
print(f"{'场景':<35} {'hRule':<10} {'val(twips)':<12} {'图片高':<10} {'判定'}")
|
||||
print("-" * 85)
|
||||
|
||||
pass_cnt = 0
|
||||
for label, row_h, h_rule_in, expected in scenarios:
|
||||
src = OUT_DIR / f"table_{h_rule_in}_{row_h}.docx"
|
||||
make_table_docx(src, [(label, 21, row_h, h_rule_in, None)])
|
||||
signed = _sign(src, height=2.0, relax_layout=True)
|
||||
root = ET.fromstring(_read_xml(signed))
|
||||
|
||||
# 找到含图片的表格行
|
||||
target_tr = None
|
||||
for p in root.iter(_ns('p', W)):
|
||||
if list(p.iter(_ns('inline', WP))):
|
||||
parent = p.getparent()
|
||||
while parent is not None and parent.tag != _ns('tr', W):
|
||||
parent = parent.getparent()
|
||||
target_tr = parent
|
||||
break
|
||||
|
||||
trPr = target_tr.find(_ns('trPr', W)) if target_tr is not None else None
|
||||
trHeight = trPr.find(_ns('trHeight', W)) if trPr is not None else None
|
||||
h_rule_out = trHeight.get(_ns('hRule', W)) if trHeight is not None else None
|
||||
val_out = trHeight.get(_ns('val', W)) if trHeight is not None else None
|
||||
|
||||
extent = list(target_tr.iter(_ns('extent', WP)))[0]
|
||||
img_h = emu_to_cm(extent.get('cy'))
|
||||
|
||||
if expected == 'relax':
|
||||
expected_rule = 'atLeast'
|
||||
expected_val = int(2.0 * 566.93 + 0.999)
|
||||
ok = (h_rule_out == expected_rule and abs(int(val_out) - expected_val) <= 1
|
||||
and abs(img_h - 2.0) < 0.05)
|
||||
verdict = f"{'✓' if ok else '✗'} 期望 atLeast/{expected_val}"
|
||||
else:
|
||||
ok = (h_rule_out == h_rule_in and abs(int(val_out) - row_h) <= 1
|
||||
and abs(img_h - 2.0) < 0.05)
|
||||
verdict = f"{'✓' if ok else '✗'} 期望保持 {h_rule_in}/{row_h}"
|
||||
if ok: pass_cnt += 1
|
||||
|
||||
print(f"{label:<35} {h_rule_out or '无':<10} {val_out or '无':<12} "
|
||||
f"{img_h:.2f}cm {verdict}")
|
||||
print(f"\nrelax 表格行高: {pass_cnt}/{len(scenarios)} 通过")
|
||||
|
||||
|
||||
def test_relax_off():
|
||||
"""relax_layout=False + line=exact 0.5cm + height=2cm → 缩放到 ≤0.5cm,不放宽。"""
|
||||
print(f"\n=== relax_layout=false 严格模式测试 ===\n")
|
||||
src = OUT_DIR / "para_strict.docx"
|
||||
make_docx(src, [("strict", 21, 283)]) # 行高 0.5cm exact
|
||||
signed = _sign(src, height=2.0, relax_layout=False)
|
||||
root = ET.fromstring(_read_xml(signed))
|
||||
|
||||
p_with_image = None
|
||||
for p in root.iter(_ns('p', W)):
|
||||
if list(p.iter(_ns('inline', WP))):
|
||||
p_with_image = p
|
||||
break
|
||||
pPr = p_with_image.find(_ns('pPr', W))
|
||||
spacing = pPr.find(_ns('spacing', W))
|
||||
line_rule = spacing.get(_ns('lineRule', W))
|
||||
line_val = spacing.get(_ns('line', W))
|
||||
extent = list(p_with_image.iter(_ns('extent', WP)))[0]
|
||||
img_h = emu_to_cm(extent.get('cy'))
|
||||
|
||||
ok = (line_rule == 'exact' and line_val == '283' and img_h <= 0.5 + 0.01)
|
||||
verdict = f"{'✓' if ok else '✗'} lineRule={line_rule}/line={line_val}/img_h={img_h:.2f}cm(期望 exact/283/≤0.5cm)"
|
||||
print(f" {verdict}")
|
||||
print(f"\nrelax off: {'1/1' if ok else '0/1'} 通过")
|
||||
|
||||
|
||||
def test_no_paragraph_font_size():
|
||||
"""段落无 <w:sz> 时,应走文档默认字号(Normal 样式 → docDefaults → 10.5pt),
|
||||
验证不会回到 default_cm=2.0cm。
|
||||
"""
|
||||
from docx import Document
|
||||
from lib import field_codes
|
||||
|
||||
print(f"\n=== 段落无 sz 字号 fallback 测试 ===\n")
|
||||
src = OUT_DIR / "no_sz.docx"
|
||||
|
||||
doc = Document()
|
||||
p = doc.add_paragraph()
|
||||
for r in field_codes.build_set_field_runs('u0'):
|
||||
p._element.append(r)
|
||||
doc.save(str(src))
|
||||
|
||||
signed = _sign(src, height=None, relax_layout=True)
|
||||
root = ET.fromstring(_read_xml(signed))
|
||||
|
||||
p_with_image = None
|
||||
for p in root.iter(_ns('p', W)):
|
||||
if list(p.iter(_ns('inline', WP))):
|
||||
p_with_image = p
|
||||
break
|
||||
extent = list(p_with_image.iter(_ns('extent', WP)))[0]
|
||||
img_h = emu_to_cm(extent.get('cy'))
|
||||
|
||||
# 模拟 _get_doc_default_font_size_pt 的优先级读 styles.xml
|
||||
styles_root = ET.fromstring(_read_xml(signed, 'word/styles.xml'))
|
||||
default_pt = None
|
||||
for style in styles_root.iter(_ns('style', W)):
|
||||
if style.get(_ns('styleId', W)) != 'Normal':
|
||||
continue
|
||||
rPr = style.find(_ns('rPr', W))
|
||||
if rPr is not None:
|
||||
sz = rPr.find(_ns('sz', W))
|
||||
if sz is not None:
|
||||
val = sz.get(_ns('val', W))
|
||||
if val:
|
||||
default_pt = int(val) / 2.0
|
||||
break
|
||||
if default_pt is None:
|
||||
doc_defaults = styles_root.find(_ns('docDefaults', W))
|
||||
if doc_defaults is not None:
|
||||
rpr_default = doc_defaults.find(_ns('rPrDefault', W))
|
||||
if rpr_default is not None:
|
||||
rpr = rpr_default.find(_ns('rPr', W))
|
||||
if rpr is not None:
|
||||
sz = rpr.find(_ns('sz', W))
|
||||
if sz is not None:
|
||||
val = sz.get(_ns('val', W))
|
||||
if val:
|
||||
default_pt = int(val) / 2.0
|
||||
|
||||
if default_pt:
|
||||
expected = round(default_pt * 2 * 2.54 / 72, 2)
|
||||
expected_src = f"文档默认{default_pt}pt×2"
|
||||
else:
|
||||
expected = round(10.5 * 2 * 2.54 / 72, 2)
|
||||
expected_src = "10.5pt fallback×2"
|
||||
|
||||
# 关键断言:不能是 default_cm=2.0
|
||||
not_default = abs(img_h - 2.0) > 0.05
|
||||
ok = not_default and abs(img_h - expected) < 0.05
|
||||
verdict = (f"{'✓' if ok else '✗'} 段落无 sz, 默认={default_pt}pt, "
|
||||
f"期望 {expected}cm ({expected_src}), 实际 {img_h:.2f}cm "
|
||||
f"(必须 ≠ 2.0cm default_cm)")
|
||||
print(f" {verdict}")
|
||||
print(f"\nno_font_size: {'1/1' if ok else '0/1'} 通过")
|
||||
|
||||
|
||||
def test_no_spacing_with_table_atleast():
|
||||
"""复刻真实 bug:段落无 spacing + 表格行 atLeast 过小 + 字号 14pt + height=2cm
|
||||
旧版 relax 函数(只处理 exact)在这种情况下是 no-op,图片被行盒子裁。
|
||||
修复后应:段落主动加 spacing line=2cm atLeast;表格行 atLeast 保持不动(atLeast 是软约束)。
|
||||
"""
|
||||
print(f"\n=== 无 spacing + 表格 atLeast 过小 (复刻用户 bug) ===\n")
|
||||
src = OUT_DIR / "no_spacing_atleast.docx"
|
||||
# 567 twips ≈ 1cm atLeast 表格行(小于目标 2cm)
|
||||
make_table_docx(src, [("表格atLeast1cm+无段落spacing", 28, 567, 'atLeast', None)])
|
||||
signed = _sign(src, height=2.0, relax_layout=True)
|
||||
root = ET.fromstring(_read_xml(signed))
|
||||
|
||||
p_with_image = None
|
||||
for p in root.iter(_ns('p', W)):
|
||||
if list(p.iter(_ns('inline', WP))):
|
||||
p_with_image = p
|
||||
break
|
||||
|
||||
pPr = p_with_image.find(_ns('pPr', W))
|
||||
spacing = pPr.find(_ns('spacing', W))
|
||||
line_rule = spacing.get(_ns('lineRule', W)) if spacing is not None else None
|
||||
line_val = spacing.get(_ns('line', W)) if spacing is not None else None
|
||||
|
||||
# 找到所在表格行
|
||||
parent = p_with_image.getparent()
|
||||
while parent is not None and parent.tag != _ns('tr', W):
|
||||
parent = parent.getparent()
|
||||
target_tr = parent
|
||||
trPr = target_tr.find(_ns('trPr', W)) if target_tr is not None else None
|
||||
trHeight = trPr.find(_ns('trHeight', W)) if trPr is not None else None
|
||||
h_rule_out = trHeight.get(_ns('hRule', W)) if trHeight is not None else None
|
||||
val_out = trHeight.get(_ns('val', W)) if trHeight is not None else None
|
||||
|
||||
extent = list(p_with_image.iter(_ns('extent', WP)))[0]
|
||||
img_h = emu_to_cm(extent.get('cy'))
|
||||
|
||||
expected_line = int(2.0 * 566.93 + 0.999)
|
||||
# 段落 spacing 应被主动创建为 atLeast/expected_line;
|
||||
# 表格行 atLeast 是软约束(不裁切),应保持原值 567
|
||||
ok = (spacing is not None
|
||||
and line_rule == 'atLeast'
|
||||
and abs(int(line_val) - expected_line) <= 1
|
||||
and h_rule_out == 'atLeast'
|
||||
and int(val_out) == 567
|
||||
and abs(img_h - 2.0) < 0.05)
|
||||
verdict = (f"{'✓' if ok else '✗'} "
|
||||
f"段落 spacing={'有' if spacing is not None else '无'}/lineRule={line_rule}/line={line_val} "
|
||||
f"(期望 atLeast/{expected_line});"
|
||||
f"表格 hRule={h_rule_out}/val={val_out}(期望保持 atLeast/567);"
|
||||
f"img_h={img_h:.2f}cm")
|
||||
print(f" {verdict}")
|
||||
print(f"\nno_spacing_atleast: {'1/1' if ok else '0/1'} 通过")
|
||||
|
||||
|
||||
def main():
|
||||
_ensure_path()
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
src_docx = OUT_DIR / "input.docx"
|
||||
|
||||
# 原 auto-size 场景
|
||||
auto_scenarios = [
|
||||
("小四+行高1cm", 21, 567),
|
||||
("小四+行高2cm", 21, 1134),
|
||||
("小四+无行高", 21, None),
|
||||
("四号+无行高", 28, None),
|
||||
("小二+无行高", 36, None),
|
||||
]
|
||||
make_docx(src_docx, auto_scenarios)
|
||||
test_auto_size(src_docx, auto_scenarios)
|
||||
|
||||
# 新增 relax 场景
|
||||
test_relax_paragraph()
|
||||
test_relax_table_row()
|
||||
test_relax_off()
|
||||
test_no_spacing_with_table_atleast()
|
||||
test_no_paragraph_font_size()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
82
image_test/compare_project_vs_test.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""对比"项目实际产出的处理图" vs "测试脚本产出的 final.png"。
|
||||
|
||||
使用前置条件:
|
||||
1. 已经在前端跑过一次签章,image_test/output/project_run/ 下有 _2_processed.png
|
||||
2. 已经跑过 image_test/process.py,image_test/output/test1/final.png 存在
|
||||
|
||||
输出:
|
||||
逐文件对比尺寸/字节/SHA256,给出"完全一致 / 仅尺寸相同 / 完全不同"的判定。
|
||||
"""
|
||||
import sys
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_RUN_DIR = BASE_DIR / "output" / "project_run"
|
||||
TEST_FINAL = BASE_DIR / "output" / "test1" / "final.png"
|
||||
|
||||
|
||||
def stats(data):
|
||||
img = Image.open(__import__('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')
|
||||
am = a.point(lambda p: 255 if p > 30 else 0)
|
||||
lm = luma.point(lambda p: 255 if p < 210 else 0)
|
||||
combined = ImageChops.darker(am, lm)
|
||||
total = img.size[0] * img.size[1] or 1
|
||||
return {
|
||||
'size': img.size,
|
||||
'bbox': combined.getbbox(),
|
||||
'content_pct': 100.0 * sum(combined.histogram()[1:]) / total,
|
||||
'sha': hashlib.sha256(data).hexdigest()[:12],
|
||||
'bytes': len(data),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
if not TEST_FINAL.exists():
|
||||
print(f"缺少基准文件: {TEST_FINAL}")
|
||||
print("请先运行: python image_test/process.py")
|
||||
sys.exit(1)
|
||||
if not PROJECT_RUN_DIR.exists():
|
||||
print(f"缺少项目运行输出目录: {PROJECT_RUN_DIR}")
|
||||
print("请先在前端跑一次签章(已重启服务)")
|
||||
sys.exit(1)
|
||||
|
||||
processed_files = sorted(PROJECT_RUN_DIR.glob("*_2_processed.png"))
|
||||
if not processed_files:
|
||||
print(f"在 {PROJECT_RUN_DIR} 下未找到 *_2_processed.png 文件")
|
||||
sys.exit(1)
|
||||
|
||||
final_data = TEST_FINAL.read_bytes()
|
||||
final_s = stats(final_data)
|
||||
print(f"=== 基准: {TEST_FINAL.name} ===")
|
||||
print(f" 尺寸={final_s['size']} bbox={final_s['bbox']} "
|
||||
f"内容={final_s['content_pct']:.1f}% bytes={final_s['bytes']} sha={final_s['sha']}")
|
||||
print()
|
||||
|
||||
print(f"=== 找到 {len(processed_files)} 个项目输出,逐个对比 ===\n")
|
||||
for p in processed_files:
|
||||
data = p.read_bytes()
|
||||
s = stats(data)
|
||||
identical = (data == final_data)
|
||||
same_size = (s['size'] == final_s['size'])
|
||||
print(f"[{p.name}]")
|
||||
print(f" 尺寸={s['size']} bbox={s['bbox']} 内容={s['content_pct']:.1f}% "
|
||||
f"bytes={s['bytes']} sha={s['sha']}")
|
||||
if identical:
|
||||
print(f" 判定: ✓ 与 final.png 逐字节完全一致")
|
||||
elif same_size:
|
||||
print(f" 判定: △ 尺寸相同但字节不同(可能只是 PNG 编码差异,目视应该一样)")
|
||||
else:
|
||||
print(f" 判定: ✗ 与 final.png 不同!尺寸={s['size']} vs {final_s['size']}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
150
image_test/e2e_test.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""端到端验证: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()
|
||||
81
image_test/extract_docx_images.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""从 docx 提取所有内嵌图片,方便定位实际插入到文档里的图片字节。
|
||||
|
||||
用法:
|
||||
python image_test/extract_docx_images.py <signed.docx>
|
||||
|
||||
输出:
|
||||
image_test/output/docx_images/<图片名>.png (按 docx 内顺序编号)
|
||||
每张图打印尺寸 + 紧致度诊断
|
||||
"""
|
||||
import io
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
OUT_DIR = BASE_DIR / "output" / "docx_images"
|
||||
|
||||
|
||||
def analyze_image(data):
|
||||
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,
|
||||
'bbox': combined.getbbox(),
|
||||
'alpha_pct': 100.0 * sum(alpha_mask.histogram()[1:]) / total,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python image_test/extract_docx_images.py <signed.docx>")
|
||||
sys.exit(1)
|
||||
|
||||
docx_path = Path(sys.argv[1])
|
||||
if not docx_path.exists():
|
||||
print(f"文件不存在: {docx_path}")
|
||||
sys.exit(1)
|
||||
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(docx_path, 'r') as z:
|
||||
media_files = sorted([n for n in z.namelist()
|
||||
if n.startswith('word/media/')])
|
||||
|
||||
if not media_files:
|
||||
print("docx 内未找到 word/media/ 下的图片")
|
||||
return
|
||||
|
||||
print(f"找到 {len(media_files)} 张图片\n")
|
||||
|
||||
for i, name in enumerate(media_files, 1):
|
||||
data = z.read(name)
|
||||
ext = Path(name).suffix or '.png'
|
||||
out_path = OUT_DIR / f"image_{i:02d}{ext}"
|
||||
with open(out_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
try:
|
||||
stats = analyze_image(data)
|
||||
print(f"[{i:02d}] {name}")
|
||||
print(f" 尺寸={stats['size']} 模式={stats['mode']} "
|
||||
f"alpha像素={stats['alpha_pct']:.1f}% "
|
||||
f"内容={stats['content_pct']:.1f}% bbox={stats['bbox']}")
|
||||
print(f" 已保存: {out_path}")
|
||||
except Exception as e:
|
||||
print(f"[{i:02d}] {name}: 解析失败 {e}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
97
image_test/find_bug.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""穷举 process_image 的参数组合,找出哪种组合会产出"未紧致裁剪"的输出。
|
||||
|
||||
逻辑:
|
||||
final.png (153x74, bbox=(0,0,153,74)) 是已知正确的紧致输出。
|
||||
遍历 is_signature/height_cm 等参数,跑 process_image,对比输出:
|
||||
- 尺寸 == (153, 74)?
|
||||
- bbox 是否贴满?
|
||||
- 字节内容是否与 final.png 完全一致?
|
||||
任何一项不一致 -> 找到 bug 触发条件。
|
||||
|
||||
输入图: image_test/input/test1.jpg
|
||||
"""
|
||||
import io
|
||||
import sys
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageChops
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
PROJECT_ROOT = BASE_DIR.parent
|
||||
INPUT_IMAGE = BASE_DIR / "input" / "test1.jpg"
|
||||
FINAL_REF = BASE_DIR / "output" / "test1" / "final.png"
|
||||
|
||||
|
||||
def _ensure_path():
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
|
||||
def _stats(data):
|
||||
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')
|
||||
am = a.point(lambda p: 255 if p > 30 else 0)
|
||||
lm = luma.point(lambda p: 255 if p < 210 else 0)
|
||||
combined = ImageChops.darker(am, lm)
|
||||
total = img.size[0] * img.size[1] or 1
|
||||
return {
|
||||
'size': img.size,
|
||||
'bbox': combined.getbbox(),
|
||||
'content_pct': 100.0 * sum(combined.histogram()[1:]) / total,
|
||||
'sha': hashlib.sha256(data).hexdigest()[:12],
|
||||
'bytes': len(data),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
_ensure_path()
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
if not INPUT_IMAGE.exists():
|
||||
print(f"缺少: {INPUT_IMAGE}")
|
||||
sys.exit(1)
|
||||
if not FINAL_REF.exists():
|
||||
print(f"缺少: {FINAL_REF},请先运行 image_test/process.py 生成")
|
||||
sys.exit(1)
|
||||
|
||||
with open(INPUT_IMAGE, 'rb') as f:
|
||||
image_bytes = f.read()
|
||||
with open(FINAL_REF, 'rb') as f:
|
||||
final_bytes = f.read()
|
||||
|
||||
final_stats = _stats(final_bytes)
|
||||
print(f"=== 参考基准 final.png ===")
|
||||
print(f" 尺寸={final_stats['size']} bbox={final_stats['bbox']} "
|
||||
f"内容={final_stats['content_pct']:.1f}% 字节={final_stats['bytes']} "
|
||||
f"sha={final_stats['sha']}")
|
||||
print()
|
||||
|
||||
from lib.image_processor import process_image
|
||||
|
||||
cases = [
|
||||
# (label, kwargs)
|
||||
("is_sig=True noise=0 rot=0", {'is_signature': True}),
|
||||
("is_sig=False noise=0 rot=0", {'is_signature': False}),
|
||||
("is_sig=True noise=3 rot=0", {'is_signature': True, 'noise_level': 3}),
|
||||
("is_sig=True noise=0 rot=-5,5",{'is_signature': True, 'rotate_min': -5, 'rotate_max': 5}),
|
||||
("is_sig=True noise=3 rot=-5,5",{'is_signature': True, 'noise_level': 3, 'rotate_min': -5, 'rotate_max': 5}),
|
||||
]
|
||||
|
||||
print(f"=== 穷举 {len(cases)} 种参数组合 ===\n")
|
||||
for label, kw in cases:
|
||||
out = process_image(image_bytes, log_source='测试', **kw)
|
||||
s = _stats(out)
|
||||
tight = (s['bbox'] == (0, 0, s['size'][0], s['size'][1]))
|
||||
same_as_final = (out == final_bytes)
|
||||
flag = "✓紧致" if tight else "✗有padding"
|
||||
flag2 = "=final" if same_as_final else "≠final"
|
||||
print(f"[{label}]")
|
||||
print(f" 尺寸={s['size']} bbox={s['bbox']} 内容={s['content_pct']:.1f}% "
|
||||
f"字节={s['bytes']} {flag} {flag2}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
BIN
image_test/input/1.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
image_test/input/81aa-icapxph6774222.jpg
Normal file
|
After Width: | Height: | Size: 192 KiB |
BIN
image_test/input/a463-icapxph6773606.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
image_test/input/a8b6-icapxph6774373.jpg
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
image_test/input/signature.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
image_test/input/test1.jpg
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
image_test/input/下载.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
100
image_test/inspect_xml.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""检查 signed.docx 中签名图片对应的 drawing XML,看实际显示尺寸(EMU)。
|
||||
|
||||
目的:
|
||||
字节已经证明一致(e2e_test.py),但用户仍看到"空白"。
|
||||
本脚本读 document.xml,找到图片所在段落的:
|
||||
- wp:extent (图片显示尺寸 EMU)
|
||||
- 段落 pPr/spacing (line 行高)
|
||||
- 段落所在表格单元格 tcW (单元格宽度)
|
||||
对比图片实际像素 vs 显示尺寸 vs 段落行高,定位空白的视觉来源。
|
||||
|
||||
EMU 换算:
|
||||
1 inch = 914400 EMU = 2.54 cm
|
||||
1 cm = 360000 EMU
|
||||
"""
|
||||
import sys
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
DOCX_PATH = BASE_DIR / "output" / "e2e" / "signed.docx"
|
||||
|
||||
W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||
WP = 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'
|
||||
A = 'http://schemas.openxmlformats.org/drawingml/2006/main'
|
||||
|
||||
|
||||
def emu_to_cm(emu):
|
||||
try:
|
||||
return int(emu) / 360000.0
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
if not DOCX_PATH.exists():
|
||||
print(f"docx 不存在: {DOCX_PATH}")
|
||||
print("请先运行: python image_test/e2e_test.py")
|
||||
sys.exit(1)
|
||||
|
||||
with zipfile.ZipFile(DOCX_PATH) as z:
|
||||
xml_bytes = z.read('word/document.xml')
|
||||
|
||||
root = ET.fromstring(xml_bytes)
|
||||
|
||||
print("=== 段落列表(含 spacing / drawing)===\n")
|
||||
for i, p in enumerate(root.iter(f'{{{W}}}p'), 1):
|
||||
pPr = p.find(f'{{{W}}}pPr')
|
||||
spacing_info = ""
|
||||
if pPr is not None:
|
||||
sp = pPr.find(f'{{{W}}}spacing')
|
||||
if sp is not None:
|
||||
line = sp.get(f'{{{W}}}line')
|
||||
rule = sp.get(f'{{{W}}}lineRule')
|
||||
before = sp.get(f'{{{W}}}before')
|
||||
after = sp.get(f'{{{W}}}after')
|
||||
if line or before or after:
|
||||
parts = []
|
||||
if line:
|
||||
cm = emu_to_cm(int(line) / 20 * 12700) if rule == 'exact' else None
|
||||
parts.append(f"line={line}({rule})->{cm:.2f}cm" if cm else f"line={line}({rule})")
|
||||
if before:
|
||||
parts.append(f"before={before}({int(before)/20:.1f}pt)")
|
||||
if after:
|
||||
parts.append(f"after={after}({int(after)/20:.1f}pt)")
|
||||
spacing_info = " | ".join(parts)
|
||||
|
||||
text = "".join(t.text or '' for t in p.iter(f'{{{W}}}t'))
|
||||
has_drawing = len(list(p.iter(f'{{{WP}}}inline'))) > 0
|
||||
marker = "[图]" if has_drawing else " "
|
||||
|
||||
print(f"[段{i:02d}] {marker} text={text[:40]!r}")
|
||||
if spacing_info:
|
||||
print(f" spacing: {spacing_info}")
|
||||
|
||||
for inline in p.iter(f'{{{WP}}}inline'):
|
||||
extent = inline.find(f'{{{WP}}}extent')
|
||||
if extent is not None:
|
||||
cx = extent.get('cx')
|
||||
cy = extent.get('cy')
|
||||
print(f" drawing extent: cx={cx}({emu_to_cm(cx):.2f}cm) cy={cy}({emu_to_cm(cy):.2f}cm)")
|
||||
|
||||
tc_parent = None
|
||||
parent = p
|
||||
while parent is not None:
|
||||
parent_iter = list(root.iter())
|
||||
break
|
||||
for tc in root.iter(f'{{{W}}}tc'):
|
||||
if p in list(tc.iter(f'{{{W}}}p')):
|
||||
tcPr = tc.find(f'{{{W}}}tcPr')
|
||||
if tcPr is not None:
|
||||
tcW = tcPr.find(f'{{{W}}}tcW')
|
||||
if tcW is not None:
|
||||
w = tcW.get(f'{{{W}}}w')
|
||||
t = tcW.get(f'{{{W}}}type')
|
||||
print(f" cell width: w={w} type={t}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
BIN
image_test/output/1/00_original.png
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
image_test/output/1/01_binarized.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
image_test/output/1/02_white_removed.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
image_test/output/1/03_border_cropped.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
image_test/output/1/04_scaled.png
Normal file
|
After Width: | Height: | Size: 863 B |
BIN
image_test/output/1/07_final_cropped.png
Normal file
|
After Width: | Height: | Size: 737 B |
15
image_test/output/1/diagnostics.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
输入: 1.jpg
|
||||
模式: RGB 尺寸: (2048, 1382)
|
||||
参数: is_sig=True height_cm=1 noise=0 rotate=[0,0] dpi=96
|
||||
阈值: alpha>30 亮度<210 二值化>128 白底>240
|
||||
--------------------------------------------------------------------------------
|
||||
[00] original size=(2048, 1382) alpha=100.0% dark= 3.2% content= 3.2% bbox=(0, 0, 2048, 1382)
|
||||
[01] binarized size=(2048, 1382) alpha=100.0% dark= 3.1% content= 3.1% bbox=(0, 0, 2044, 1381)
|
||||
[02] white_removed size=(2048, 1382) alpha= 3.1% dark= 3.1% content= 3.1% bbox=(0, 0, 2044, 1381)
|
||||
[03] border_cropped size=(2044, 1381) alpha= 3.1% dark= 3.1% content= 3.1% bbox=(0, 0, 2044, 1381)
|
||||
[04] scaled size=(54, 37) alpha= 8.9% dark=100.0% content= 8.9% bbox=(10, 12, 42, 32)
|
||||
[05] noised SKIPPED
|
||||
[06] rotated SKIPPED
|
||||
[07] final_cropped size=(32, 20) alpha= 27.8% dark=100.0% content= 27.8% bbox=(0, 0, 32, 20)
|
||||
--------------------------------------------------------------------------------
|
||||
最终输出: final.png size=(32, 20)
|
||||
BIN
image_test/output/1/final.png
Normal file
|
After Width: | Height: | Size: 737 B |
BIN
image_test/output/81aa-icapxph6774222/00_original.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
image_test/output/81aa-icapxph6774222/01_binarized.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
image_test/output/81aa-icapxph6774222/02_white_removed.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
image_test/output/81aa-icapxph6774222/03_border_cropped.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
image_test/output/81aa-icapxph6774222/04_scaled.png
Normal file
|
After Width: | Height: | Size: 995 B |
BIN
image_test/output/81aa-icapxph6774222/07_final_cropped.png
Normal file
|
After Width: | Height: | Size: 896 B |
15
image_test/output/81aa-icapxph6774222/diagnostics.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
输入: 81aa-icapxph6774222.jpg
|
||||
模式: RGB 尺寸: (2048, 1382)
|
||||
参数: is_sig=True height_cm=1 noise=0 rotate=[0,0] dpi=96
|
||||
阈值: alpha>30 亮度<210 二值化>128 白底>240
|
||||
--------------------------------------------------------------------------------
|
||||
[00] original size=(2048, 1382) alpha=100.0% dark= 4.1% content= 4.1% bbox=(0, 0, 1743, 1374)
|
||||
[01] binarized size=(2048, 1382) alpha=100.0% dark= 3.9% content= 3.9% bbox=(0, 0, 1686, 1222)
|
||||
[02] white_removed size=(2048, 1382) alpha= 3.9% dark= 3.9% content= 3.9% bbox=(0, 0, 1686, 1222)
|
||||
[03] border_cropped size=(1686, 1222) alpha= 5.4% dark= 5.4% content= 5.4% bbox=(0, 0, 1686, 1222)
|
||||
[04] scaled size=(51, 37) alpha= 12.8% dark=100.0% content= 12.8% bbox=(13, 17, 51, 36)
|
||||
[05] noised SKIPPED
|
||||
[06] rotated SKIPPED
|
||||
[07] final_cropped size=(38, 19) alpha= 33.4% dark=100.0% content= 33.4% bbox=(0, 0, 38, 19)
|
||||
--------------------------------------------------------------------------------
|
||||
最终输出: final.png size=(38, 19)
|
||||
BIN
image_test/output/81aa-icapxph6774222/final.png
Normal file
|
After Width: | Height: | Size: 896 B |
BIN
image_test/output/a463-icapxph6773606/00_original.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
image_test/output/a463-icapxph6773606/01_binarized.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
image_test/output/a463-icapxph6773606/02_white_removed.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
image_test/output/a463-icapxph6773606/03_border_cropped.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
image_test/output/a463-icapxph6773606/04_scaled.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
image_test/output/a463-icapxph6773606/07_final_cropped.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
15
image_test/output/a463-icapxph6773606/diagnostics.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
输入: a463-icapxph6773606.jpg
|
||||
模式: RGB 尺寸: (2048, 1382)
|
||||
参数: is_sig=True height_cm=1 noise=0 rotate=[0,0] dpi=96
|
||||
阈值: alpha>30 亮度<210 二值化>128 白底>240
|
||||
--------------------------------------------------------------------------------
|
||||
[00] original size=(2048, 1382) alpha=100.0% dark= 3.0% content= 3.0% bbox=(501, 471, 1742, 1063)
|
||||
[01] binarized size=(2048, 1382) alpha=100.0% dark= 2.8% content= 2.8% bbox=(502, 471, 1741, 1063)
|
||||
[02] white_removed size=(2048, 1382) alpha= 2.8% dark= 2.8% content= 2.8% bbox=(502, 471, 1741, 1063)
|
||||
[03] border_cropped size=(1239, 592) alpha= 11.0% dark= 11.0% content= 11.0% bbox=(0, 0, 1239, 592)
|
||||
[04] scaled size=(77, 37) alpha= 19.6% dark=100.0% content= 19.6% bbox=(0, 0, 77, 37)
|
||||
[05] noised SKIPPED
|
||||
[06] rotated SKIPPED
|
||||
[07] final_cropped size=(77, 37) alpha= 19.6% dark=100.0% content= 19.6% bbox=(0, 0, 77, 37)
|
||||
--------------------------------------------------------------------------------
|
||||
最终输出: final.png size=(77, 37)
|
||||
BIN
image_test/output/a463-icapxph6773606/final.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
image_test/output/a8b6-icapxph6774373/00_original.png
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
image_test/output/a8b6-icapxph6774373/01_binarized.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
image_test/output/a8b6-icapxph6774373/02_white_removed.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
image_test/output/a8b6-icapxph6774373/03_border_cropped.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
image_test/output/a8b6-icapxph6774373/04_scaled.png
Normal file
|
After Width: | Height: | Size: 893 B |
BIN
image_test/output/a8b6-icapxph6774373/07_final_cropped.png
Normal file
|
After Width: | Height: | Size: 661 B |
15
image_test/output/a8b6-icapxph6774373/diagnostics.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
输入: a8b6-icapxph6774373.jpg
|
||||
模式: RGB 尺寸: (2048, 1382)
|
||||
参数: is_sig=True height_cm=1 noise=0 rotate=[0,0] dpi=96
|
||||
阈值: alpha>30 亮度<210 二值化>128 白底>240
|
||||
--------------------------------------------------------------------------------
|
||||
[00] original size=(2048, 1382) alpha=100.0% dark= 4.4% content= 4.4% bbox=(0, 0, 2048, 1382)
|
||||
[01] binarized size=(2048, 1382) alpha=100.0% dark= 4.2% content= 4.2% bbox=(0, 0, 2048, 1382)
|
||||
[02] white_removed size=(2048, 1382) alpha= 4.2% dark= 4.2% content= 4.2% bbox=(0, 0, 2048, 1382)
|
||||
[03] border_cropped size=(2048, 1382) alpha= 4.2% dark= 4.2% content= 4.2% bbox=(0, 0, 2048, 1382)
|
||||
[04] scaled size=(54, 37) alpha= 8.6% dark=100.0% content= 8.6% bbox=(16, 13, 44, 37)
|
||||
[05] noised SKIPPED
|
||||
[06] rotated SKIPPED
|
||||
[07] final_cropped size=(28, 24) alpha= 25.4% dark=100.0% content= 25.4% bbox=(0, 0, 28, 24)
|
||||
--------------------------------------------------------------------------------
|
||||
最终输出: final.png size=(28, 24)
|
||||
BIN
image_test/output/a8b6-icapxph6774373/final.png
Normal file
|
After Width: | Height: | Size: 661 B |
BIN
image_test/output/auto_size/input.docx
Normal file
BIN
image_test/output/auto_size/no_spacing_atleast.docx
Normal file
BIN
image_test/output/auto_size/no_sz.docx
Normal file
BIN
image_test/output/auto_size/para_283.docx
Normal file
BIN
image_test/output/auto_size/para_2835.docx
Normal file
BIN
image_test/output/auto_size/para_strict.docx
Normal file
BIN
image_test/output/auto_size/signed.docx
Normal file
BIN
image_test/output/auto_size/signed_auto.docx
Normal file
BIN
image_test/output/auto_size/table_atLeast_567.docx
Normal file
BIN
image_test/output/auto_size/table_exact_567.docx
Normal file
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 101 B |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 18 KiB |