check-num-metrics.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """数字字体度量核对(票 17 维护者走查项:「大屏 KPI 数字不跳格」)。
  4. 规格 11 Q3:数字字体不可授权时,回退备选需与设计字体**度量兼容**,避免大屏数字列宽跳动。
  5. 本脚本把「跳格」拆成两个可测的事实,并明确哪一个由本脚本断言、哪一个必须人工走查:
  6. ① 等宽性(本脚本断言):数字 0–9 在**同一字体内部**是否等宽。宽差越大,数值刷新时
  7. 整串数字的总宽变化越大——这才是「跳格」的成因。若新字体的内部宽差**不比旧字体更差**,
  8. 换字就没有引入新的抖动来源。
  9. ② 逐字差(本脚本**只报告、不断言**):新旧字体在同一字符上的 advance 差。这些差值
  10. 是否造成可见观感变化,属人工语义判断(走查),规格 12 明令不转自动断言——
  11. 这里只把数字摆出来,写进票 06 缺口清单供走查核对,不假装代码能判定观感。
  12. 用法:
  13. set FONTTOOLS_LIBS=<fontTools 安装目录>
  14. python tools/check-num-metrics.py
  15. 退出码非 0 仅表示「换字后数字抖动比旧字体更差」。
  16. """
  17. import os
  18. import sys
  19. FT_LIBS = os.environ.get('FONTTOOLS_LIBS')
  20. if FT_LIBS:
  21. sys.path.insert(0, FT_LIBS)
  22. from fontTools.ttLib import TTFont # noqa: E402
  23. HERE = os.path.dirname(os.path.abspath(__file__))
  24. ROOT = os.path.abspath(os.path.join(HERE, '..', '..', '..'))
  25. DIGITS = '0123456789'
  26. EXTRA = '.%:+-/()'
  27. INCUMBENT = ('Bahnschrift(旧 / 系统数字字体)', r'C:\Windows\Fonts\bahnschrift.ttf')
  28. CANDIDATES = [
  29. ('Rajdhani Subset 400(新,--num 首位)',
  30. os.path.join(ROOT, 'public', 'cockpit', 'assets', 'fonts', 'rajdhani-400.woff2')),
  31. ('Rajdhani Subset 700(新)',
  32. os.path.join(ROOT, 'public', 'cockpit', 'assets', 'fonts', 'rajdhani-700.woff2')),
  33. ('Share Tech Mono Subset(新,--num 次位)',
  34. os.path.join(ROOT, 'public', 'cockpit', 'assets', 'fonts', 'share-tech-mono-400.woff2')),
  35. ]
  36. def advances(path, chars):
  37. if not os.path.exists(path):
  38. return None
  39. font = TTFont(path, lazy=True)
  40. try:
  41. upm = font['head'].unitsPerEm
  42. cmap = font.getBestCmap()
  43. hmtx = font['hmtx']
  44. return {ch: hmtx[cmap[ord(ch)]][0] / upm for ch in chars if ord(ch) in cmap}
  45. finally:
  46. font.close()
  47. def main():
  48. base = advances(INCUMBENT[1], DIGITS + EXTRA)
  49. if base is None:
  50. print('FAIL 基准字体缺失:', INCUMBENT[1])
  51. return 1
  52. base_spread = max(base[c] for c in DIGITS) - min(base[c] for c in DIGITS)
  53. deviations = []
  54. regressions = []
  55. for label, path in CANDIDATES:
  56. adv = advances(path, DIGITS + EXTRA)
  57. if adv is None:
  58. print(f'SKIP {label}: 文件不存在')
  59. continue
  60. spread = max(adv[c] for c in DIGITS) - min(adv[c] for c in DIGITS)
  61. worse = spread > base_spread
  62. print(f'{label}')
  63. print(f' 数字内部宽差 {spread:.3f} em (旧 {base_spread:.3f} em) '
  64. + ('更差 -> 回归' if worse else '不更差 -> 未引入新抖动'))
  65. if worse:
  66. regressions.append(f'{label}: 数字内部宽差 {spread:.3f} em > 旧 {base_spread:.3f} em')
  67. for ch in DIGITS + EXTRA:
  68. if ch in base and ch in adv:
  69. d = adv[ch] - base[ch]
  70. if abs(d) > 0.05:
  71. deviations.append((label, ch, d))
  72. print()
  73. print(f'逐字差 > 0.05 em 的字符({len(deviations)} 项,属人工走查判定的观感偏差,须登记票 06 缺口清单):')
  74. for label, ch, d in deviations:
  75. print(f' {label:38s} {ch!r} {d:+.3f} em')
  76. if not deviations:
  77. print(' (无)')
  78. print()
  79. if regressions:
  80. print('FAIL(新字体数字抖动比旧字体更差):')
  81. for r in regressions:
  82. print(' -', r)
  83. return 1
  84. print(f'PASS 未引入新的数字抖动(各新字体数字内部宽差均不劣于 {INCUMBENT[0]});'
  85. f'逐字差 {len(deviations)} 项已列出,交维护者走查判定')
  86. return 0
  87. if __name__ == '__main__':
  88. sys.exit(main())