Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
112 lines
3.5 KiB
Python
112 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
||
"""Strip in-figure titles from checked-in SVGs and crop top whitespace.
|
||
|
||
按學術規範:圖本身不包含標題(標題寫在正文)。本指令碼保守地處理
|
||
`images/` 中已簽入的 SVG 檔案。
|
||
|
||
判定規則(保守):
|
||
- 第一處出現的 <text> 元素,若同時滿足:font-size>=20 且 y<60 且 text-anchor=middle,
|
||
則視為圖示題——刪除該元素,並將 viewBox 上沿和 height 各下移/縮小 CROP_PX。
|
||
- 否則該檔案不做任何修改。
|
||
|
||
用法:python3 strip_titles.py [--dry-run]
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
import sys
|
||
|
||
IMAGES_DIR = os.path.join(os.path.dirname(__file__), 'images')
|
||
CROP_PX = 40
|
||
|
||
TEXT_RE = re.compile(
|
||
r'<text\b([^>]*?)>(.*?)</text>',
|
||
flags=re.DOTALL,
|
||
)
|
||
ATTR_RE = re.compile(r'(\w[\w-]*)\s*=\s*"([^"]*)"')
|
||
SVG_TAG_RE = re.compile(r'<svg\b([^>]*)>', flags=re.DOTALL)
|
||
VIEWBOX_RE = re.compile(r'viewBox="([^"]+)"')
|
||
HEIGHT_ATTR_RE = re.compile(r'\bheight="([^"]+)"')
|
||
|
||
|
||
def parse_attrs(attr_str):
|
||
return {m.group(1): m.group(2) for m in ATTR_RE.finditer(attr_str)}
|
||
|
||
|
||
def process_file(path, dry_run=False):
|
||
with open(path, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
# 找第一個 <text>
|
||
m = TEXT_RE.search(content)
|
||
if not m:
|
||
return 'no-text'
|
||
attrs = parse_attrs(m.group(1))
|
||
fs = float(attrs.get('font-size', '0') or 0)
|
||
y = float(attrs.get('y', '0') or 0)
|
||
anchor = attrs.get('text-anchor', '')
|
||
if not (fs >= 20 and y < 60 and anchor == 'middle'):
|
||
return 'no-title'
|
||
|
||
# 刪除該 <text> 元素
|
||
new_content = content[: m.start()] + content[m.end():]
|
||
|
||
# 調整 viewBox 與 height
|
||
svg_m = SVG_TAG_RE.search(new_content)
|
||
if not svg_m:
|
||
return 'no-svg-tag'
|
||
svg_attrs_str = svg_m.group(1)
|
||
|
||
def replace_viewbox(vbm):
|
||
parts = vbm.group(1).split()
|
||
if len(parts) != 4:
|
||
return vbm.group(0)
|
||
x0, y0, w, h = parts
|
||
try:
|
||
new_y0 = float(y0) + CROP_PX
|
||
new_h = float(h) - CROP_PX
|
||
except ValueError:
|
||
return vbm.group(0)
|
||
def fmt(v):
|
||
return str(int(v)) if v == int(v) else str(v)
|
||
return f'viewBox="{fmt(float(x0))} {fmt(new_y0)} {fmt(float(w))} {fmt(new_h)}"'
|
||
|
||
new_svg_attrs = VIEWBOX_RE.sub(replace_viewbox, svg_attrs_str)
|
||
|
||
def replace_height(hm):
|
||
try:
|
||
new_h = float(hm.group(1)) - CROP_PX
|
||
new_h_s = str(int(new_h)) if new_h == int(new_h) else str(new_h)
|
||
return f'height="{new_h_s}"'
|
||
except ValueError:
|
||
return hm.group(0)
|
||
|
||
new_svg_attrs = HEIGHT_ATTR_RE.sub(replace_height, new_svg_attrs, count=1)
|
||
new_content = new_content[:svg_m.start()] + f'<svg{new_svg_attrs}>' + new_content[svg_m.end():]
|
||
|
||
if not dry_run:
|
||
with open(path, 'w', encoding='utf-8') as f:
|
||
f.write(new_content)
|
||
return 'stripped'
|
||
|
||
|
||
def main():
|
||
dry_run = '--dry-run' in sys.argv
|
||
counts = {'stripped': 0, 'no-title': 0, 'no-text': 0, 'no-svg-tag': 0}
|
||
examples = {k: [] for k in counts}
|
||
for name in sorted(os.listdir(IMAGES_DIR)):
|
||
if not name.endswith('.svg'):
|
||
continue
|
||
result = process_file(os.path.join(IMAGES_DIR, name), dry_run=dry_run)
|
||
counts[result] = counts.get(result, 0) + 1
|
||
if len(examples[result]) < 5:
|
||
examples[result].append(name)
|
||
mode = '[DRY-RUN]' if dry_run else '[APPLIED]'
|
||
print(f'{mode} summary:')
|
||
for k, v in counts.items():
|
||
print(f' {k}: {v} e.g. {examples[k]}')
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|