修复签章浮动靠左问题

This commit is contained in:
lihansani
2026-07-20 16:43:08 +08:00
parent 7affeaa253
commit bc8e564aa7
701 changed files with 71380 additions and 25 deletions

View File

@@ -484,6 +484,222 @@ def test_no_spacing_with_table_atleast():
print(f"\nno_spacing_atleast: {'1/1' if ok else '0/1'} 通过")
def _sign_with_mode(docx_path, height, relax_layout, signature_mode, stamp_mode, is_signature):
"""扩展 _sign支持 signature_mode/stamp_mode 透传 + 控制 is_signature。"""
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': is_signature}]
params = {
'docx_name': 'input.docx',
'match_mode': 'upload',
'stamps': stamps,
'use_global_height': True,
'height': height,
'relax_layout': relax_layout,
'signature_mode': signature_mode,
'stamp_mode': stamp_mode,
}
result, err = sign_word(docx_data, params, log_source='mode测试', trace_id='mode')
if err:
print(f"sign_word 失败: {err}")
sys.exit(1)
return result['data']
def _find_image_root(root):
"""找到含 inline 或 anchor 图片的 <w:p>。"""
for p in root.iter(_ns('p', W)):
if list(p.iter(_ns('inline', WP))) or list(p.iter(_ns('anchor', WP))):
return p
return None
def test_signature_inline_default():
"""签名默认走 inlineis_signature=True + 不传 mode → <wp:inline>。"""
print(f"\n=== 签名默认 inline 模式(回归)===\n")
src = OUT_DIR / "sig_inline.docx"
make_docx(src, [("签名默认inline", 21, 283)]) # 行高 exact 0.5cm
# 不传 signature_mode/stamp_mode → 应走默认值signature_mode='inline'
signed = _sign_with_mode(src, height=2.0, relax_layout=True,
signature_mode='inline', stamp_mode='anchor',
is_signature=True)
root = ET.fromstring(_read_xml(signed))
p = _find_image_root(root)
inlines = list(p.iter(_ns('inline', WP))) if p is not None else []
anchors = list(p.iter(_ns('anchor', WP))) if p is not None else []
ok = len(inlines) == 1 and len(anchors) == 0
verdict = (f"{'' if ok else ''} inline={len(inlines)}, anchor={len(anchors)} "
f"(期望 inline=1, anchor=0")
print(f" {verdict}")
print(f"\nsignature_inline_default: {'1/1' if ok else '0/1'} 通过")
def test_signature_anchor_mode():
"""签名切到 anchoris_signature=True + signature_mode='anchor' → <wp:anchor>。
anchor 不触发 relax_layout段落 spacing 应保持原状exact/283"""
print(f"\n=== 签名 anchor 模式(浮动覆盖)===\n")
src = OUT_DIR / "sig_anchor.docx"
make_docx(src, [("签名anchor", 21, 283)]) # 行高 exact 0.5cm
signed = _sign_with_mode(src, height=2.0, relax_layout=True,
signature_mode='anchor', stamp_mode='anchor',
is_signature=True)
root = ET.fromstring(_read_xml(signed))
p = _find_image_root(root)
inlines = list(p.iter(_ns('inline', WP))) if p is not None else []
anchors = list(p.iter(_ns('anchor', WP))) if p is not None else []
pPr = p.find(_ns('pPr', W)) if p 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
# 断言1anchor 元素存在
has_anchor = len(anchors) == 1 and len(inlines) == 0
# 断言2anchor 不触发 relax —— spacing 应保持原 exact/283
not_relaxed = (line_rule == 'exact' and line_val == '283')
# 断言3anchor 专属属性
anchor_elem = anchors[0] if anchors else None
behind_doc = anchor_elem.get('behindDoc') if anchor_elem is not None else None
allow_overlap = anchor_elem.get('allowOverlap') if anchor_elem is not None else None
has_wrap_none = anchor_elem is not None and anchor_elem.find(_ns('wrapNone', WP)) is not None
pos_h = anchor_elem.find(_ns('positionH', WP)) if anchor_elem is not None else None
pos_v = anchor_elem.find(_ns('positionV', WP)) if anchor_elem is not None else None
# 期望posH relativeFrom='paragraph' + posOffset=0贴段落起点char 模式不可靠)
# posV relativeFrom='paragraph' + posOffset=0
h_off_el = pos_h.find(_ns('posOffset', WP)) if pos_h is not None else None
h_off_val = int(h_off_el.text) if h_off_el is not None and h_off_el.text else None
has_pos = (pos_h is not None and pos_v is not None
and pos_h.get('relativeFrom') == 'paragraph'
and pos_v.get('relativeFrom') == 'paragraph'
and h_off_val == 0)
ok = has_anchor and not_relaxed and behind_doc == '0' and allow_overlap == '1' and has_wrap_none and has_pos
verdict = (f"{'' if ok else ''} anchor={len(anchors)}, inline={len(inlines)}; "
f"spacing lineRule={line_rule}/line={line_val}(期望保持 exact/283 未放宽); "
f"behindDoc={behind_doc}, allowOverlap={allow_overlap}, "
f"wrapNone={'' if has_wrap_none else ''}, "
f"posH relativeFrom=paragraph/posOffset={h_off_val}, "
f"posV relativeFrom=paragraph")
print(f" {verdict}")
print(f"\nsignature_anchor_mode: {'1/1' if ok else '0/1'} 通过")
def test_stamp_anchor_default():
"""盖章默认走 anchoris_signature=False + 不传 mode → <wp:anchor>。"""
print(f"\n=== 盖章默认 anchor 模式(浮动覆盖)===\n")
src = OUT_DIR / "stamp_anchor.docx"
make_docx(src, [("盖章默认anchor", 21, 283)])
signed = _sign_with_mode(src, height=2.0, relax_layout=True,
signature_mode='inline', stamp_mode='anchor',
is_signature=False)
root = ET.fromstring(_read_xml(signed))
p = _find_image_root(root)
inlines = list(p.iter(_ns('inline', WP))) if p is not None else []
anchors = list(p.iter(_ns('anchor', WP))) if p is not None else []
anchor_elem = anchors[0] if anchors else None
behind_doc = anchor_elem.get('behindDoc') if anchor_elem is not None else None
has_wrap_none = anchor_elem is not None and anchor_elem.find(_ns('wrapNone', WP)) is not None
ok = len(anchors) == 1 and len(inlines) == 0 and behind_doc == '0' and has_wrap_none
verdict = (f"{'' if ok else ''} anchor={len(anchors)}, inline={len(inlines)}, "
f"behindDoc={behind_doc}, wrapNone={'' if has_wrap_none else ''} "
f"(期望 anchor=1, inline=0, behindDoc=0")
print(f" {verdict}")
print(f"\nstamp_anchor_default: {'1/1' if ok else '0/1'} 通过")
def test_stamp_inline_override():
"""盖章切到 inlineis_signature=False + stamp_mode='inline' → <wp:inline>。"""
print(f"\n=== 盖章切到 inline强制嵌入===\n")
src = OUT_DIR / "stamp_inline.docx"
make_docx(src, [("盖章强制inline", 21, 283)])
signed = _sign_with_mode(src, height=2.0, relax_layout=True,
signature_mode='inline', stamp_mode='inline',
is_signature=False)
root = ET.fromstring(_read_xml(signed))
p = _find_image_root(root)
inlines = list(p.iter(_ns('inline', WP))) if p is not None else []
anchors = list(p.iter(_ns('anchor', WP))) if p is not None else []
# inline 模式下 spacing 应被放宽为 atLeast
pPr = p.find(_ns('pPr', W)) if p 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
ok = len(inlines) == 1 and len(anchors) == 0 and line_rule == 'atLeast'
verdict = (f"{'' if ok else ''} inline={len(inlines)}, anchor={len(anchors)}, "
f"spacing lineRule={line_rule}(期望 inline=1, anchor=0, lineRule=atLeast 被放宽)")
print(f" {verdict}")
print(f"\nstamp_inline_override: {'1/1' if ok else '0/1'} 通过")
def test_anchor_offset_estimation():
"""SET 域前有 CJK 文字时anchor 模式 posOffset 应反映前文字宽度估算。
场景:段落 "签字人:" + SET 域4 个 CJK + 1 全角冒号 = 5 个 CJK 宽度字符)
字号 14pt四号half_pt=28→ 期望偏移 ≈ 5 × 14pt × 12700 EMU/pt = 889000 EMU
"""
from docx import Document
from lib import field_codes
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
print(f"\n=== anchor 模式 posOffset 估算测试 ===\n")
src = OUT_DIR / "anchor_offset.docx"
doc = Document()
p = doc.add_paragraph()
# 前缀文字 "签字人:"
run = p.add_run("签字人:")
rPr = run._element.get_or_add_rPr()
sz = OxmlElement('w:sz')
sz.set(qn('w:val'), '28') # 14pt
rPr.append(sz)
for r in field_codes.build_set_field_runs('u0'):
p._element.append(r)
doc.save(str(src))
signed = _sign_with_mode(src, height=2.0, relax_layout=True,
signature_mode='anchor', stamp_mode='anchor',
is_signature=True)
root = ET.fromstring(_read_xml(signed))
p_with_image = None
for para in root.iter(_ns('p', W)):
if list(para.iter(_ns('anchor', WP))):
p_with_image = para
break
anchor = list(p_with_image.iter(_ns('anchor', WP)))[0]
pos_h = anchor.find(_ns('positionH', WP))
h_off_el = pos_h.find(_ns('posOffset', WP))
h_off_val = int(h_off_el.text) if h_off_el is not None and h_off_el.text else 0
# "签字人:" = 3 个 CJK 字符 + 1 个全角冒号 = 4 个 CJK 宽度
# 期望偏移 = 4 × 14pt × 12700 EMU/pt = 711200 EMU容差 ±15%
expected_emu = 4 * 14 * 12700
tolerance = int(expected_emu * 0.15)
ok = (pos_h.get('relativeFrom') == 'paragraph'
and abs(h_off_val - expected_emu) <= tolerance
and h_off_val > 0)
verdict = (f"{'' if ok else ''} posH relativeFrom={pos_h.get('relativeFrom')}, "
f"posOffset={h_off_val} EMU (≈{h_off_val / 360000:.2f}cm), "
f"期望 ≈{expected_emu} EMU (≈{expected_emu / 360000:.2f}cm, 容差 ±15%)")
print(f" {verdict}")
print(f"\nanchor_offset_estimation: {'1/1' if ok else '0/1'} 通过")
def main():
_ensure_path()
sys.stdout.reconfigure(encoding='utf-8')
@@ -509,6 +725,13 @@ def main():
test_no_spacing_with_table_atleast()
test_no_paragraph_font_size()
# 新增 anchor/inline 模式切换场景
test_signature_inline_default()
test_signature_anchor_mode()
test_stamp_anchor_default()
test_stamp_inline_override()
test_anchor_offset_estimation()
if __name__ == '__main__':
main()