文字没变,格式却变了:一次PPT局部加粗漏检的实测

举报
yd_225778114 发表于 2026/10/17 13:47:27 2026/10/17
【摘要】 用一个唯一文本框与三组对照,复现文字内容相同但尾部加粗被漏检的问题,定位按run下标比较的遗漏,并说明字符区间比较、继承样式及回归验收的边界。

比较两份PPT时,“拼出来的字符串相同”和“视觉样式相同”是两个不同的问题。今天在PPT-diff的实测中,我遇到一个很小却容易漏掉的例子:一个文本框原来全部是普通字重,新版只把最后几个字符加粗,文字一字未改。解析器已经读到了加粗属性,最终报告却没有字体变化。

这次记录只讨论这一处可复现的比较问题,不评价整个工具,也不把“建议怎样修复”写成“已经修复”。公开复现及固定提交见GitCode的agent-best-practices/PPT-diff仓库Issue #48。

一段文字为什么会变成两个run

PowerPoint里的一个段落可以由多个文字片段组成。每个片段称为run,它带有自己的文字和字符格式。只选中后半段文字加粗时,段落不需要增加任何字符,内部片段边界却可能改变。

这次合成样例没有学校、公司或真实文档内容,仅包含一个名为UniqueBody的普通文本框。它的位置、字号、字体名称和颜色都显式固定。旧版和新版的完整文字都为Total:

版本 片段0 片段1
旧版 普通 Total 无
新版 普通 To 加粗 tal

先单独检查解析结果,可以看到新版确实得到两个run,bold值分别为False和True。这样就能把问题范围缩小:这次不是因为加粗属性根本没读出来,而是读取后如何配对、比较和汇总的问题。

这里显式指定了字体属性,因此没有把继承样式问题混入测试。python-pptx的官方文档说明,Font.bold返回None时表示该属性需要从样式层级继承,不能直接等同于False。真实工具如果要比较“最终显示样式”,还需要额外处理这些继承关系。出处为python-pptx官方文档的Text-related objects章节、Font objects小节、Font.bold条目。

用三个对照缩小问题范围

单个失败样例只能说明某种输入没有得到预期结果,不能证明原因。因此我又准备了两个对照,三组输入共用同一个旧版:

新版变化 预期字体变化 实测font_changes 实测modified
把普通Total拆为普通To和普通tal 无 0 0
普通To和加粗tal 有 0 0
整个Total直接加粗 有 1 1

第一个对照说明,拆分run本身不应被当成视觉变化。第三个对照说明,这个测试环境和普通整段加粗路径能够识别字体修改。真正异常的是第二行:文字相同,尾部字符的字重不同,解析器也读到了不同值,但比较结果没有体现出来。

实测环境为Python 3.12.14、python-pptx 1.0.2;PPT-diff的main固定在db6c75892650854abf663c91b66ead486fde1299。所有文件均由脚本生成,没有使用个人演示文稿。

复现代码怎样保持可核对

下面复现会在仓库根目录生成四份很小的PPTX,直接调用项目原有解析和比较函数。它不修改项目源码,也不自动启动PowerPoint或浏览器。

from pathlib import Path
import sys
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor

sys.path.insert(0, str(Path("scripts").resolve()))
from ppt_parser import parse_pptx
from comparator import compare_pptx, generate_summary

out = Path("repro_tail_style")
out.mkdir(exist_ok=True)

def save(name, values):
    prs = Presentation()
    slide = prs.slides.add_slide(prs.slide_layouts[6])
    box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(5), Inches(1))
    box.name = "UniqueBody"
    p = box.text_frame.paragraphs[0]
    for text, bold in values:
        r = p.add_run()
        r.text = text
        r.font.bold = bold
        r.font.italic = False
        r.font.underline = False
        r.font.name = "Arial"
        r.font.size = Pt(24)
        r.font.color.rgb = RGBColor(0, 0, 0)
    path = out / (name + ".pptx")
    prs.save(path)
    return parse_pptx(str(path))

old = save("old", [("Total", False)])
for name, values in [
    ("same_style", [("To", False), ("tal", False)]),
    ("bold_tail", [("To", False), ("tal", True)]),
    ("whole_bold", [("Total", True)]),
]:
    new = save(name, values)
    s = generate_summary(compare_pptx(old, new))
    print(name, "font_changes=", s["font_changes"], "modified=", s["modified"])
    print("parsed_bold=", [r.bold for r in new[0].shapes[0].paragraphs[0].runs])

实际输出为:

same_style font_changes= 0 modified= 0
parsed_bold= [False, False]
bold_tail font_changes= 0 modified= 0
parsed_bold= [False, True]
whole_bold font_changes= 1 modified= 1
parsed_bold= [True]

当前版本还有既有Issue #16提到的页状态问题,因此前两个对照会出现moved=1。本次没有把它重新算成新发现,也没有根据moved状态推导字体变化,而是分别检查字体计数、解析属性和modified计数。

漏检发生在新增片段分支

检查comparator.py中的_diff_font_runs(),可以看到它按run下标比较。两边都存在时逐项比较字体属性;只有旧run时也会生成记录;但只有新run的情况没有对应处理。

本例中,下标0比较的是旧版普通Total和新版普通To,两者显式字体属性相同,所以没有变化。下标1只有新版加粗tal,恰好落入没有处理的情况。随后整个页面的汇总也就没有收到字体变化。

这与“同名形状匹配错误”是不同层次的问题。本例仅有一个名字唯一的文本框,不存在多个同名形状交叉配对;也与三种输出模式对共享结果消费不一致不同,因为这次共享比较结果本身就没有产生应有的字体变化。

为什么不能简单地把新run都报成修改

看见缺少分支,最直接的做法似乎是:新版多一个run,就追加一条字体修改。但这样会把第一个对照也报成变化。普通To加普通tal与普通Total的显示样式相同,结构拆分并不构成格式差异。

更稳妥的方向是先保留段落边界和完整文字,在可确定字符对应关系的区间上比较有效样式。对于文字完全相同的段落,可以按字符偏移表达样式区间,再合并相邻且样式相同的区间。片段边界不同、覆盖区间的样式相同,就不应报变化;对应字符的有效样式不同,才产生变化记录。

如果文字同时被插入、删除或替换,则需要先做文本对齐,不能直接比较相同下标的片段。空run、换行、字段和主题继承也需要单独定义口径。这里提出的是后续设计方向,不是已经完成并验证过的修复补丁。

验收应覆盖边界,也应覆盖反例

修复后至少应保留五类检查:尾部局部加粗能够检出,整段加粗仍能检出,仅拆分同样式run不误报,反向取消加粗能够检出,同一文件自比较不产生字体变化。再逐步增加中间字符、跨多个run以及文本同时修改的场景。

这次实测最有价值的地方,是把“看起来没报变化”拆成可核对的证据链:输入怎样构造,解析器读到了什么,比较函数输出什么,哪些对照通过,哪些边界仍未验证。这样既能帮助维护者定位问题,也能避免用一个简单分支修补出新的误报。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。