看门狗 §3.2 流程图重绘为 SVG:任务置位→校验者判定→喂狗 + 时间轴
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
| `gen_frame_svg.py` | 协议帧格式图 | RemoDispBus 帧结构 |
|
||||
| `gen_cfg_arch_svg.py` | 配置引擎架构图 | 配置分层与持久化 |
|
||||
| `gen_partition_svg.py` | 分区图 | 存储分区 |
|
||||
| `gen_watchdog_svg.py` | 看门狗心跳模型图 | IWDG + 任务心跳位图(置位→判定→喂狗 + 时间轴) |
|
||||
|
||||
## 使用方式
|
||||
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/看门狗心跳模型.svg —— IWDG + 任务心跳位图流程图(纯标准库)。
|
||||
|
||||
对应 docs/子系统设计/看门狗设计说明.md §3.2「采用方案:
|
||||
任务置位 → 校验者判定 → 校验者喂狗」。
|
||||
|
||||
结构:左右两栏 + 底部时间轴
|
||||
左 = 各任务循环(生产者):s_hb_bits |= (1u << MY_ID),仅置位不清零
|
||||
右 = defaultTask 唯一喂狗者(消费者):100ms 抽样 → 取走清零 → 掩码判定
|
||||
全中 → 喂狗;有缺 → 累计 miss_streak(容忍 8 次)
|
||||
下 = 停喂 → IWDG 自然溢出 → 复位(时间轴:0 / 0.8s / 1.7s / 2.5s)
|
||||
|
||||
为什么不用 Mermaid:原 ASCII 图是"生产者-消费者 + 判定分支",需要精确对齐
|
||||
位图行与判定框,且时间轴的"包含关系"必须按比例表达,Mermaid 做不到。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_watchdog_svg.py
|
||||
输出:
|
||||
docs/images/看门狗心跳模型.svg
|
||||
改流程时:编辑本文件顶部的 TASKS / BITS / POLL_STEPS / TIMELINE 数据后重跑。
|
||||
"""
|
||||
import html
|
||||
import math
|
||||
import os
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "看门狗心跳模型.svg")
|
||||
|
||||
# ----------------------------- 画布与样式 -----------------------------
|
||||
W = 1440
|
||||
PAD = 40
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
PANEL_TITLE_FONT = 15
|
||||
CARD_TITLE_FONT = 14
|
||||
CARD_NOTE_FONT = 11.5
|
||||
LABEL_FONT = 11.5
|
||||
FOOT_FONT = 12
|
||||
|
||||
# 调色板(docs/架构设计/图形绘制流程.md §3.1)
|
||||
C_PROD, BG_PROD = "#2563eb", "#eff6ff" # 蓝:生产者(任务置位)
|
||||
C_BIT, BG_BIT = "#0d9488", "#f0fdfa" # 青:心跳位图
|
||||
C_CONS, BG_CONS = "#7c3aed", "#f5f3ff" # 紫:校验者(喂狗者)
|
||||
C_OK = "#16a34a" # 绿:喂狗
|
||||
C_WARN, BG_WARN = "#d97706", "#fffbeb" # 琥珀:累计缺失
|
||||
C_FAIL, BG_FAIL = "#e11d48", "#fff1f2" # 玫红:停喂 / 复位
|
||||
|
||||
ARROW_W = 2.2
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
# 任务(生产者):bit = 心跳位序号(task_watchdog.h 的枚举值)
|
||||
TASKS = [
|
||||
dict(name="defaultTask", bit=0, note="配置属主 / syslog 唯一写者 · 50ms 基准"),
|
||||
dict(name="netTask", bit=1, note="net_poll 循环 · 1ms 节拍"),
|
||||
dict(name="ftpTask", bit=2, note="lftpd 事件循环(需在 lftpd 侧埋点)"),
|
||||
dict(name="uiTask", bit=4, note="LVGL 主循环 · 10ms 节拍"),
|
||||
dict(name="timeSyncTask", bit=5, note="NTP 轮询 · ≤50ms"),
|
||||
]
|
||||
|
||||
# 必需掩码(wd_required_mask):5 位全要;bit3 = testTask 不在默认掩码
|
||||
REQ_BITS = [0, 1, 2, 4, 5]
|
||||
|
||||
# 校验者判定步骤(右栏自上而下)
|
||||
POLL_STEPS = [
|
||||
dict(title="① 节流:每 100ms 抽样一次",
|
||||
note="now - last_tick < 100ms 则直接返回(defaultTask 50ms 循环的整数倍)",
|
||||
color=C_CONS, bg=BG_CONS),
|
||||
dict(title="② 取走并清零(单字原子)",
|
||||
note="snapshot = s_hb_bits; s_hb_bits = 0; ← 读-改-写天然原子,免临界区",
|
||||
color=C_BIT, bg=BG_BIT),
|
||||
dict(title="③ 掩码判定",
|
||||
note="snapshot == 0x37(bit0|1|2|4|5 全中)?",
|
||||
color=C_CONS, bg=BG_CONS),
|
||||
]
|
||||
|
||||
# 判定分支结果
|
||||
BRANCH_OK = dict(title="全员到位 → 喂狗",
|
||||
note="s_miss_streak = 0",
|
||||
note2="IWDG->KR = 0xAAAA;",
|
||||
color=C_OK, bg="#f0fdf4")
|
||||
BRANCH_NG = dict(title="有缺席 → 累计,不喂狗",
|
||||
note="s_miss_total++ / streak++",
|
||||
note2="连续 8 次 → 打 [WD] task stall 后停喂",
|
||||
color=C_WARN, bg=BG_WARN)
|
||||
|
||||
# 时间轴刻度(秒)与事件
|
||||
T_AXIS = [0.0, 0.8, 1.7, 2.5]
|
||||
TIMELINE = [
|
||||
dict(seg=(0.0, 0.8), label="仍照常喂狗", sub="miss_streak 1 → 8", color=C_OK),
|
||||
dict(seg=(0.8, 1.7), label="已停喂 → 等 IWDG 自然溢出", sub="worst case(LSI 47kHz)", color=C_FAIL),
|
||||
dict(seg=(1.7, 2.5), label="标称溢出窗口", sub="LSI 32kHz", color=C_WARN),
|
||||
]
|
||||
|
||||
FOOT = ("喂狗 = \"所有被监视任务都在 100ms 窗口内动过\";任一任务卡死 → 位图长期残缺 → "
|
||||
"停喂 → IWDG 复位(保留 IWDGRSTF 权威归因)。对应 task_watchdog.c / 设计说明 §3.2。")
|
||||
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def text_w(s, fs):
|
||||
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
|
||||
w = 0.0
|
||||
for ch in s:
|
||||
w += fs * (1.0 if ord(ch) >= 0x2E80 else 0.58)
|
||||
return w
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
out = []
|
||||
add = out.append
|
||||
|
||||
def line(x1, y1, x2, y2, color, head=True, width=ARROW_W, dash=False):
|
||||
d = ' stroke-dasharray="7 5"' if dash else ''
|
||||
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" '
|
||||
'stroke-width="%.1f"%s/>' % (x1, y1, x2, y2, color, width, d))
|
||||
if head:
|
||||
ang = math.atan2(y2 - y1, x2 - x1)
|
||||
hl, hw = 11.0, 5.5
|
||||
bx, by = x2 - hl * math.cos(ang), y2 - hl * math.sin(ang)
|
||||
px, py = -math.sin(ang) * hw, math.cos(ang) * hw
|
||||
add('<path d="M %.1f %.1f L %.1f %.1f L %.1f %.1f z" fill="%s"/>'
|
||||
% (x2, y2, bx + px, by + py, bx - px, by - py, color))
|
||||
|
||||
def box(x, y, w, h, color, bg, title, note, title_fs=CARD_TITLE_FONT,
|
||||
note_fs=CARD_NOTE_FONT, stroke_op=0.55, note2=None):
|
||||
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="10" '
|
||||
'fill="%s" stroke="%s" stroke-opacity="%.2f"/>'
|
||||
% (x, y, w, h, bg, color, stroke_op))
|
||||
add('<text x="%.1f" y="%.1f" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
|
||||
% (x + 14, y + 24, title_fs, esc(title)))
|
||||
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
|
||||
% (x + 14, y + 24 + note_fs + 7, note_fs, esc(note)))
|
||||
if note2:
|
||||
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
|
||||
% (x + 14, y + 24 + (note_fs + 7) * 2, note_fs, esc(note2)))
|
||||
|
||||
# ---- 布局参数 ----
|
||||
PANEL_Y = 148
|
||||
PANEL_H = 584 # 由左栏内容(5 任务 + 心跳位图带 + 位格)反推
|
||||
LB_X, LB_W = PAD, 470 # 左栏:任务(生产者)
|
||||
RB_X = LB_X + LB_W + 34 # 右栏:校验者(消费者)
|
||||
RB_W = W - PAD - RB_X
|
||||
|
||||
TASK_H, TASK_GAP = 62, 18
|
||||
sx = LB_X + 22
|
||||
sw = LB_W - 44
|
||||
|
||||
# ---- 标题 ----
|
||||
add('<text x="%d" y="44" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
|
||||
% (PAD, TITLE_FONT, esc("看门狗心跳模型 · 任务置位 → 校验者判定 → 校验者喂狗")))
|
||||
add('<text x="%d" y="70" font-size="%d" fill="#64748b">%s</text>'
|
||||
% (PAD, SUB_FONT,
|
||||
esc("IWDG + 单字心跳位图 · 由 defaultTask 独占喂狗权(唯一喂狗者)")))
|
||||
|
||||
# ---- 图例 ----
|
||||
lx, ly = PAD, 110
|
||||
for color, label in [(C_PROD, "生产者(任务置位)"), (C_BIT, "心跳位图"),
|
||||
(C_CONS, "校验者(喂狗者)"), (C_OK, "喂狗"),
|
||||
(C_FAIL, "停喂 / 复位")]:
|
||||
add('<rect x="%d" y="%d" width="16" height="16" rx="4" fill="#ffffff" '
|
||||
'stroke="%s" stroke-width="2"/>' % (lx, ly - 12, color))
|
||||
lx += 22
|
||||
add('<text x="%d" y="%d" font-size="12" fill="#475569">%s</text>' % (lx, ly, esc(label)))
|
||||
lx += text_w(label, 12) + 30
|
||||
|
||||
# ---- 左栏:任务(生产者) ----
|
||||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
|
||||
'stroke="#64748b" stroke-opacity="0.28"/>' % (LB_X, PANEL_Y, LB_W, PANEL_H))
|
||||
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
|
||||
% (LB_X + 20, PANEL_Y + 30, PANEL_TITLE_FONT,
|
||||
esc("各任务主循环(生产者)—— 每轮只置位,不清零")))
|
||||
add('<text x="%d" y="%d" font-size="13" font-weight="700" fill="%s">%s</text>'
|
||||
% (LB_X + 20, PANEL_Y + 54, C_PROD, esc("s_hb_bits |= (1u << MY_ID)")))
|
||||
|
||||
task_y = []
|
||||
y = PANEL_Y + 72
|
||||
for t in TASKS:
|
||||
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="9" fill="%s" '
|
||||
'stroke="%s" stroke-opacity="0.5"/>' % (sx, y, sw, TASK_H, BG_PROD, C_PROD))
|
||||
# 位号徽标
|
||||
add('<rect x="%.1f" y="%.1f" width="34" height="24" rx="6" fill="%s"/>'
|
||||
% (sx + 12, y + 10, C_PROD))
|
||||
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="#ffffff" '
|
||||
'text-anchor="middle">%s</text>'
|
||||
% (sx + 29, y + 26.5, esc("b" + str(t["bit"]))))
|
||||
add('<text x="%.1f" y="%.1f" font-size="%d" font-weight="700" fill="#0f172a">%s</text>'
|
||||
% (sx + 56, y + 26, CARD_TITLE_FONT, esc(t["name"])))
|
||||
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="#64748b">%s</text>'
|
||||
% (sx + 56, y + 46, CARD_NOTE_FONT, esc(t["note"])))
|
||||
task_y.append(y + TASK_H / 2.0)
|
||||
y += TASK_H + TASK_GAP
|
||||
|
||||
# ---- 心跳位图(左栏底部横带) ----
|
||||
bm_y = y + 6
|
||||
bm_h = 48
|
||||
add('<rect x="%.1f" y="%.1f" width="%.1f" height="%d" rx="9" fill="%s" '
|
||||
'stroke="%s" stroke-opacity="0.6"/>' % (sx, bm_y, sw, bm_h, BG_BIT, C_BIT))
|
||||
add('<text x="%.1f" y="%.1f" font-size="12" font-weight="700" fill="%s">%s</text>'
|
||||
% (sx + 14, bm_y + 20, C_BIT, esc("心跳位图 s_hb_bits(32 位单字)")))
|
||||
add('<text x="%.1f" y="%.1f" font-size="11" fill="#64748b">%s</text>'
|
||||
% (sx + 14, bm_y + 38, esc("就绪掩码 0x37 = bit0|1|2|4|5(bit3=testTask 默认不在内)")))
|
||||
# 各位方格
|
||||
bx0 = sx + 14
|
||||
bw, bgap = 34, 6
|
||||
for i in range(6):
|
||||
cx = bx0 + i * (bw + bgap)
|
||||
cy = bm_y + bm_h + 8
|
||||
on = i in REQ_BITS
|
||||
add('<rect x="%.1f" y="%.1f" width="%d" height="22" rx="4" fill="%s" '
|
||||
'stroke="%s" stroke-opacity="%.2f"/>'
|
||||
% (cx, cy, bw, BG_BIT if on else "#f1f5f9", C_BIT if on else "#94a3b8",
|
||||
0.7 if on else 0.4))
|
||||
add('<text x="%.1f" y="%.1f" font-size="11" font-weight="700" fill="%s" '
|
||||
'text-anchor="middle">%s</text>'
|
||||
% (cx + bw / 2.0, cy + 15, C_BIT if on else "#94a3b8", esc(str(i))))
|
||||
|
||||
# 各任务 → 位图:右侧竖直总线汇入位图带右缘
|
||||
bus_x = sx + sw - 4
|
||||
for t in TASKS:
|
||||
ty = task_y[TASKS.index(t)]
|
||||
line(sx + sw, ty, bus_x, ty, C_PROD, head=False)
|
||||
line(bus_x, task_y[0], bus_x, bm_y + bm_h / 2.0, C_PROD, head=False)
|
||||
line(bus_x, bm_y + bm_h / 2.0, sx + sw, bm_y + bm_h / 2.0, C_PROD)
|
||||
|
||||
# ---- 右栏:校验者(defaultTask) ----
|
||||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
|
||||
'stroke="#64748b" stroke-opacity="0.28"/>' % (RB_X, PANEL_Y, RB_W, PANEL_H))
|
||||
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
|
||||
% (RB_X + 20, PANEL_Y + 30, PANEL_TITLE_FONT,
|
||||
esc("defaultTask 主循环(唯一喂狗者 · 消费者)")))
|
||||
|
||||
st_x = RB_X + 22
|
||||
st_w = RB_W - 44
|
||||
STEP_H, STEP_GAP2 = 62, 40
|
||||
sy = PANEL_Y + 56
|
||||
step_cy = []
|
||||
for i, s in enumerate(POLL_STEPS):
|
||||
box(st_x, sy, st_w, STEP_H, s["color"], s["bg"], s["title"], s["note"])
|
||||
step_cy.append(sy + STEP_H / 2.0)
|
||||
if i < len(POLL_STEPS) - 1:
|
||||
line(st_x + st_w / 2, sy + STEP_H, st_x + st_w / 2, sy + STEP_H + STEP_GAP2,
|
||||
s["color"])
|
||||
sy += STEP_H + STEP_GAP2
|
||||
|
||||
# 位图 → 校验者 ②(取走清零):正交走线(右出 → 上折 → 入盒)
|
||||
tap_y = bm_y + bm_h / 2.0
|
||||
line(sx + sw, tap_y, st_x - 22, tap_y, C_BIT, head=False)
|
||||
line(st_x - 22, tap_y, st_x - 22, step_cy[1], C_BIT, head=False)
|
||||
line(st_x - 22, step_cy[1], st_x, step_cy[1], C_BIT)
|
||||
add('<text x="%.1f" y="%.1f" font-size="%.1f" fill="%s" text-anchor="middle">%s</text>'
|
||||
% (sx + sw + 6, tap_y - 8, LABEL_FONT, C_BIT, esc("快照")))
|
||||
|
||||
# ---- 判定分支:喂狗 / 累计停喂 ----
|
||||
br_y = sy - STEP_GAP2 + 16
|
||||
br_w = (st_w - 24) / 2.0
|
||||
ok_x, ng_x = st_x, st_x + br_w + 24
|
||||
|
||||
# 分支引线:从 ③ 下沿分叉(fork 线画在 ③ 之下 8px 处,避免压住盒底)
|
||||
fork_y = step_cy[2] + STEP_H / 2.0
|
||||
line(st_x + st_w / 2, fork_y, st_x + st_w / 2, fork_y + 14, C_CONS, head=False)
|
||||
line(st_x + br_w / 2.0, fork_y + 14, ok_x + br_w / 2.0, fork_y + 14, C_OK, head=False)
|
||||
line(st_x + st_w / 2, fork_y + 14, ng_x + br_w / 2.0, fork_y + 14, C_WARN, head=False)
|
||||
line(ok_x + br_w / 2.0, fork_y + 14, ok_x + br_w / 2.0, br_y, C_OK)
|
||||
line(ng_x + br_w / 2.0, fork_y + 14, ng_x + br_w / 2.0, br_y, C_WARN)
|
||||
|
||||
add('<text x="%.1f" y="%.1f" font-size="11.5" font-weight="700" fill="%s" '
|
||||
'text-anchor="middle">%s</text>'
|
||||
% (ok_x + br_w / 2.0, br_y - 6, C_OK, esc("是")))
|
||||
add('<text x="%.1f" y="%.1f" font-size="11.5" font-weight="700" fill="%s" '
|
||||
'text-anchor="middle">%s</text>'
|
||||
% (ng_x + br_w / 2.0, br_y - 6, C_WARN, esc("否")))
|
||||
|
||||
br_h = 84
|
||||
box(ok_x, br_y, br_w, br_h, BRANCH_OK["color"], BRANCH_OK["bg"],
|
||||
BRANCH_OK["title"], BRANCH_OK["note"], title_fs=13, note_fs=10.5,
|
||||
note2=BRANCH_OK["note2"])
|
||||
box(ng_x, br_y, br_w, br_h, BRANCH_NG["color"], BRANCH_NG["bg"],
|
||||
BRANCH_NG["title"], BRANCH_NG["note"], title_fs=13, note_fs=10.5,
|
||||
note2=BRANCH_NG["note2"])
|
||||
|
||||
# ---- 时间轴 ----
|
||||
tl_y = PANEL_Y + PANEL_H + 34
|
||||
tl_h = 150
|
||||
add('<rect x="%d" y="%d" width="%d" height="%d" rx="14" fill="#f8fafc" '
|
||||
'stroke="#64748b" stroke-opacity="0.28"/>' % (PAD, tl_y, W - 2 * PAD, tl_h))
|
||||
add('<text x="%d" y="%d" font-size="%.1f" font-weight="700" fill="#334155">%s</text>'
|
||||
% (PAD + 20, tl_y + 28, PANEL_TITLE_FONT,
|
||||
esc("停喂之后:时间轴上的三个时刻(0.8s 停喂 → 1.7s 最坏溢出 → 2.5s 标称)")))
|
||||
|
||||
ax_x0, ax_x1 = PAD + 60, W - PAD - 60
|
||||
ax_y = tl_y + 62
|
||||
span = T_AXIS[-1]
|
||||
def ax_pos(t):
|
||||
return ax_x0 + (ax_x1 - ax_x0) * (t / span)
|
||||
|
||||
# 分段色带
|
||||
for it in TIMELINE:
|
||||
s0, s1 = it["seg"]
|
||||
x0, x1 = ax_pos(s0), ax_pos(s1)
|
||||
add('<rect x="%.1f" y="%.1f" width="%.1f" height="14" rx="3" fill="%s" '
|
||||
'opacity="0.75"/>' % (x0, ax_y, x1 - x0, it["color"], ))
|
||||
add('<text x="%.1f" y="%.1f" font-size="11.5" fill="%s" text-anchor="middle">%s</text>'
|
||||
% ((x0 + x1) / 2.0, ax_y + 34, it["color"], esc(it["label"])))
|
||||
add('<text x="%.1f" y="%.1f" font-size="10.5" fill="#94a3b8" text-anchor="middle">%s</text>'
|
||||
% ((x0 + x1) / 2.0, ax_y + 50, esc(it["sub"])))
|
||||
|
||||
# 刻度
|
||||
for t in T_AXIS:
|
||||
x = ax_pos(t)
|
||||
line(x, ax_y - 6, x, ax_y + 14, "#475569", head=False, width=1.6)
|
||||
add('<text x="%.1f" y="%.1f" font-size="11" font-weight="700" fill="#475569" '
|
||||
'text-anchor="middle">%ss</text>' % (x, ax_y - 12, esc(("%g" % t))))
|
||||
|
||||
# 停喂点竖标 + 裕度
|
||||
x_stop, x_worst = ax_pos(0.8), ax_pos(1.7)
|
||||
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
|
||||
'stroke-dasharray="5 4"/>' % (x_stop, ax_y - 4, x_stop, ax_y + 20, C_FAIL))
|
||||
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
|
||||
'stroke-dasharray="5 4"/>' % (x_worst, ax_y - 4, x_worst, ax_y + 20, C_WARN))
|
||||
add('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="1.6" '
|
||||
'stroke-dasharray="3 3"/>' % (x_stop, ax_y + 68, x_worst, ax_y + 68, C_OK))
|
||||
add('<text x="%.1f" y="%.1f" font-size="11" fill="%s" text-anchor="middle">%s</text>'
|
||||
% ((x_stop + x_worst) / 2.0, ax_y + 84, C_OK, esc("裕度 ≈ 900ms")))
|
||||
|
||||
# ---- 页脚 ----
|
||||
total_h = tl_y + tl_h + 34
|
||||
add('<text x="%d" y="%d" font-size="%d" fill="#64748b">%s</text>'
|
||||
% (PAD, total_h - 14, FOOT_FONT, esc(FOOT)))
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||||
% (W, total_h, W, total_h, FONT, "\n".join(out)))
|
||||
return svg, total_h
|
||||
|
||||
|
||||
def main():
|
||||
svg, h = build()
|
||||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(svg)
|
||||
import xml.etree.ElementTree as ET
|
||||
ET.fromstring(svg)
|
||||
print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user