修复签章浮动靠左问题
This commit is contained in:
@@ -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():
|
||||
"""签名默认走 inline:is_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():
|
||||
"""签名切到 anchor:is_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
|
||||
|
||||
# 断言1:anchor 元素存在
|
||||
has_anchor = len(anchors) == 1 and len(inlines) == 0
|
||||
# 断言2:anchor 不触发 relax —— spacing 应保持原 exact/283
|
||||
not_relaxed = (line_rule == 'exact' and line_val == '283')
|
||||
# 断言3:anchor 专属属性
|
||||
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():
|
||||
"""盖章默认走 anchor:is_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():
|
||||
"""盖章切到 inline:is_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()
|
||||
|
||||
Reference in New Issue
Block a user