262 lines
11 KiB
Python
262 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
||
from pathlib import Path
|
||
import math
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
|
||
BASE_DIR = Path(__file__).resolve().parent
|
||
OUT = BASE_DIR / "MESCommonBase流程图.png"
|
||
|
||
W, H = 5200, 3800
|
||
img = Image.new("RGB", (W, H), "#F7F9FC")
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
FONT_CANDIDATES = [
|
||
r"C:\Windows\Fonts\NotoSansSC-VF.ttf",
|
||
r"C:\Windows\Fonts\simhei.ttf",
|
||
r"C:\Windows\Fonts\Deng.ttf",
|
||
r"C:\Windows\Fonts\simsun.ttc",
|
||
]
|
||
|
||
|
||
def load_font(size, bold=False):
|
||
candidates = []
|
||
if bold:
|
||
candidates.extend([r"C:\Windows\Fonts\Dengb.ttf", r"C:\Windows\Fonts\simhei.ttf"])
|
||
candidates.extend(FONT_CANDIDATES)
|
||
for path in candidates:
|
||
try:
|
||
return ImageFont.truetype(path, size=size)
|
||
except Exception:
|
||
pass
|
||
return ImageFont.load_default()
|
||
|
||
|
||
font_title = load_font(72, True)
|
||
font_subtitle = load_font(34)
|
||
font_box = load_font(31)
|
||
font_box_small = load_font(27)
|
||
font_panel_title = load_font(42, True)
|
||
font_note = load_font(28)
|
||
font_edge = load_font(26)
|
||
|
||
|
||
def text_size(text, font):
|
||
bbox = draw.textbbox((0, 0), text, font=font)
|
||
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||
|
||
|
||
def wrap_line(line, font, max_width):
|
||
if not line:
|
||
return [""]
|
||
lines = []
|
||
current = ""
|
||
for ch in line:
|
||
test = current + ch
|
||
if text_size(test, font)[0] <= max_width or not current:
|
||
current = test
|
||
else:
|
||
lines.append(current)
|
||
current = ch
|
||
if current:
|
||
lines.append(current)
|
||
return lines
|
||
|
||
|
||
def wrap_text(text, font, max_width):
|
||
lines = []
|
||
for raw in text.split("\n"):
|
||
lines.extend(wrap_line(raw, font, max_width))
|
||
return lines
|
||
|
||
|
||
def draw_multiline_center(text, cx, cy, max_width, font, fill="#172033", line_gap=12):
|
||
lines = wrap_text(text, font, max_width)
|
||
heights = [text_size(line, font)[1] for line in lines]
|
||
total_h = sum(heights) + line_gap * (len(lines) - 1)
|
||
y = cy - total_h / 2
|
||
for line, h in zip(lines, heights):
|
||
width, _ = text_size(line, font)
|
||
draw.text((cx - width / 2, y), line, font=font, fill=fill)
|
||
y += h + line_gap
|
||
|
||
|
||
def draw_multiline_left(text, x, y, max_width, font, fill="#172033", line_gap=10):
|
||
lines = wrap_text(text, font, max_width)
|
||
yy = y
|
||
for line in lines:
|
||
draw.text((x, yy), line, font=font, fill=fill)
|
||
yy += text_size(line, font)[1] + line_gap
|
||
return yy
|
||
|
||
|
||
def box_bounds(cx, cy, w, h):
|
||
return (cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2)
|
||
|
||
|
||
def draw_box(cx, cy, w, h, text, fill="#FFFFFF", outline="#2E5A8A", width=5, font=None, radius=34):
|
||
font = font or font_box
|
||
bounds = box_bounds(cx, cy, w, h)
|
||
draw.rounded_rectangle(bounds, radius=radius, fill=fill, outline=outline, width=width)
|
||
draw_multiline_center(text, cx, cy, w - 70, font)
|
||
return bounds
|
||
|
||
|
||
def draw_diamond(cx, cy, w, h, text, fill="#FFF4CC", outline="#A66F00", width=5, font=None):
|
||
font = font or font_box
|
||
points = [(cx, cy - h / 2), (cx + w / 2, cy), (cx, cy + h / 2), (cx - w / 2, cy)]
|
||
draw.polygon(points, fill=fill, outline=outline)
|
||
for i in range(width):
|
||
offset = i * 0.9
|
||
border_points = [
|
||
(cx, cy - h / 2 + offset),
|
||
(cx + w / 2 - offset, cy),
|
||
(cx, cy + h / 2 - offset),
|
||
(cx - w / 2 + offset, cy),
|
||
]
|
||
draw.line(border_points + [border_points[0]], fill=outline, width=1)
|
||
draw_multiline_center(text, cx, cy, w - 80, font)
|
||
return points
|
||
|
||
|
||
def draw_panel(x, y, w, h, title, fill="#FFFFFF", outline="#B9C4D6"):
|
||
draw.rounded_rectangle((x, y, x + w, y + h), radius=34, fill=fill, outline=outline, width=5)
|
||
draw.text((x + 45, y + 28), title, font=font_panel_title, fill="#172033")
|
||
|
||
|
||
def arrow(points, color="#344054", width=6, label=None, label_pos=None):
|
||
for p1, p2 in zip(points, points[1:]):
|
||
draw.line((p1[0], p1[1], p2[0], p2[1]), fill=color, width=width)
|
||
x1, y1 = points[-2]
|
||
x2, y2 = points[-1]
|
||
angle = math.atan2(y2 - y1, x2 - x1)
|
||
size = 24
|
||
left = (x2 - size * math.cos(angle - math.pi / 7), y2 - size * math.sin(angle - math.pi / 7))
|
||
right = (x2 - size * math.cos(angle + math.pi / 7), y2 - size * math.sin(angle + math.pi / 7))
|
||
draw.polygon([(x2, y2), left, right], fill=color)
|
||
if label:
|
||
lx, ly = label_pos if label_pos else ((x1 + x2) / 2, (y1 + y2) / 2)
|
||
tw, th = text_size(label, font_edge)
|
||
draw.rounded_rectangle(
|
||
(lx - tw / 2 - 14, ly - th / 2 - 8, lx + tw / 2 + 14, ly + th / 2 + 8),
|
||
radius=12,
|
||
fill="#F7F9FC",
|
||
)
|
||
draw.text((lx - tw / 2, ly - th / 2), label, font=font_edge, fill=color)
|
||
|
||
|
||
def draw_title():
|
||
main_title = "MESCommonBase.ashx 主流程图"
|
||
subtitle = "入口解析 -> Type 分发 -> DataLink/ExcelWebCall 执行 -> 响应输出"
|
||
tw, _ = text_size(main_title, font_title)
|
||
draw.text(((W - tw) / 2, 45), main_title, font=font_title, fill="#111827")
|
||
tw, _ = text_size(subtitle, font_subtitle)
|
||
draw.text(((W - tw) / 2, 132), subtitle, font=font_subtitle, fill="#475467")
|
||
|
||
|
||
def draw_top_flow():
|
||
cx = W / 2
|
||
draw_box(cx, 285, 900, 125, "HTTP 请求\nMES_Manage/submit/MESCommonBase.ashx", "#E8F1FF", "#2864B4", font=font_box)
|
||
draw_box(cx, 475, 1320, 145, "ProcessRequest\n设置 application/json,读取 Request.InputStream", "#E8F1FF", "#2864B4", font=font_box)
|
||
draw_box(cx, 670, 1320, 145, "JsonMapper.ToObject(stream)\n尝试读取 Type / type", "#EAF7EF", "#227A45", font=font_box)
|
||
draw_box(cx, 865, 1520, 145, "JavaScriptSerializer.Deserialize<jsonobj>(stream)\n成功后用 dataobj.Type 覆盖 type", "#EAF7EF", "#227A45", font=font_box)
|
||
draw_diamond(cx, 1090, 520, 190, "dataobj\n== null?", "#FFF4CC", "#B27700", font=font_box)
|
||
draw_box(3890, 1090, 840, 130, '读取 Request["param"]\n再次解析 Type / type', "#FFF7E6", "#B27700", font=font_box_small)
|
||
draw_diamond(cx, 1315, 500, 180, "switch\n(type)", "#FFF4CC", "#B27700", font=font_box)
|
||
|
||
arrow([(cx, 348), (cx, 402)])
|
||
arrow([(cx, 548), (cx, 598)])
|
||
arrow([(cx, 742), (cx, 792)])
|
||
arrow([(cx, 938), (cx, 995)])
|
||
arrow([(cx + 260, 1090), (3470, 1090)], label="是", label_pos=(3140, 1050))
|
||
arrow([(3890, 1155), (3890, 1240), (2850, 1240), (2850, 1315)])
|
||
arrow([(cx, 1185), (cx, 1225)], label="否", label_pos=(2555, 1210))
|
||
|
||
|
||
def draw_branch_panel():
|
||
panel_x, panel_y, panel_w, panel_h = 160, 1460, 4880, 1120
|
||
draw_panel(panel_x, panel_y, panel_w, panel_h, "Type 分发分支", fill="#FFFFFF", outline="#C7D2E5")
|
||
arrow([(W / 2, 1405), (W / 2, panel_y + 10)])
|
||
|
||
card_w, card_h = 1080, 330
|
||
xs = [790, 1960, 3130, 4300]
|
||
y1, y2 = 1740, 2205
|
||
cards = [
|
||
(xs[0], y1, "2001 Excel 导出\nExcelWebCall.ExcelFile(jsonData)\n生成 bytes / fileName / extension\n响应:二进制下载", "#EAF7EF", "#227A45"),
|
||
(xs[1], y1, "2002 PDF 导出\nExcelWebCall.ExcelFilePdf(jsonData)\nExcel 转 PDF\n响应:二进制下载", "#EAF7EF", "#227A45"),
|
||
(xs[2], y1, "2003 合成 Excel 图片/扩展名导出\n读取 Form 第一项 dataimg[0]\nExcelWebCall.ExcelFile(jsonData)\n响应:二进制下载", "#EAF7EF", "#227A45"),
|
||
(xs[3], y1, "2004 数据库文件下载\nDataLink.ExePROCEDURE_Type2004\n返回 bytes / fileName / extension\n响应:二进制下载", "#EAF7EF", "#227A45"),
|
||
(xs[0], y2, "15 文件上传\n读取 Request.Files[0]\n拆分 name / suffix / bytes\nDataLink.ExePROCEDURE_Type15\n响应:JSON", "#EEF2FF", "#4F46E5"),
|
||
(xs[1], y2, "16 数据库文件下载\nDataLink.ExePROCEDURE_Type16\n返回 fileName / suffix / bytes\nbytes 为 null 时直接 return", "#EAF7EF", "#227A45"),
|
||
(xs[2], y2, "4000 IP 查询\n读取 ServerVariables / UserHostAddress\n响应:文本 IP", "#FFF7E6", "#B27700"),
|
||
(xs[3], y2, "default 通用业务调用\nDataLink.SqlWebCall(type,jsonData,dataobj)\n进入数据库/存储过程二次分发", "#F3E8FF", "#7E22CE"),
|
||
]
|
||
for cx, cy, text, fill, outline in cards:
|
||
draw_box(cx, cy, card_w, card_h, text, fill, outline, font=font_box_small, radius=28)
|
||
|
||
return panel_x, panel_y, panel_w, panel_h, xs, y2, card_h
|
||
|
||
|
||
def draw_detail_panels(panel_info):
|
||
panel_x, panel_y, panel_w, panel_h, xs, y2, card_h = panel_info
|
||
resp_x, resp_y, resp_w, resp_h = 260, 2740, 2050, 740
|
||
detail_x, detail_y, detail_w, detail_h = 2520, 2740, 2420, 740
|
||
|
||
draw_panel(resp_x, resp_y, resp_w, resp_h, "响应输出", fill="#FFFFFF", outline="#C7D2E5")
|
||
resp_text = (
|
||
"文件分支 2001/2002/2003/2004/16:\n"
|
||
" application/octet-stream + Content-Disposition\n"
|
||
" BinaryWrite(bytes) + Flush/End\n\n"
|
||
"Type 15 与 default:\n"
|
||
" Response.Write(JSON 文本)\n\n"
|
||
"Type 4000:\n"
|
||
" Response.Write(userIP)\n\n"
|
||
"外层 catch:异常时写当前 responseText,初始值为 NULL"
|
||
)
|
||
draw_multiline_left(resp_text, resp_x + 60, resp_y + 115, resp_w - 120, font_note, fill="#344054", line_gap=12)
|
||
|
||
draw_panel(detail_x, detail_y, detail_w, detail_h, "DataLink.SqlWebCall 默认分支", fill="#FFFFFF", outline="#C7D2E5")
|
||
detail_text = (
|
||
"初始化:InitSystemReg 读取 Web.config 的 ConnectionString\n\n"
|
||
"8888 / 5001 / 5002:加密、注册/改密、登录并生成 token\n\n"
|
||
"1 / 2 / 5:旧协议存储过程,Param 为拼接字符串\n\n"
|
||
"11 / 111 / 12 / 13 / 21:新协议存储过程,Param 为 JSON 数组字符串\n\n"
|
||
"1001 / 1002 / 3 / 4 / 7 / 22 / 3001:直接 SQL、建表导入或 SQL 命令执行\n\n"
|
||
"输出:数组 JSON 或 { code, message, data }"
|
||
)
|
||
draw_multiline_left(detail_text, detail_x + 60, detail_y + 115, detail_w - 120, font_note, fill="#344054", line_gap=12)
|
||
|
||
arrow([(xs[3], y2 + card_h / 2), (xs[3], 2630), (detail_x + detail_w / 2, 2630), (detail_x + detail_w / 2, detail_y)], color="#7E22CE")
|
||
arrow([(panel_x + 1150, panel_y + panel_h), (panel_x + 1150, resp_y)], color="#227A45")
|
||
arrow([(detail_x, detail_y + detail_h / 2), (resp_x + resp_w, detail_y + detail_h / 2)], color="#7E22CE")
|
||
|
||
return resp_x, resp_y, resp_w, resp_h
|
||
|
||
|
||
def draw_note_and_end(resp_panel):
|
||
resp_x, resp_y, resp_w, resp_h = resp_panel
|
||
note_x, note_y, note_w, note_h = 520, 3545, 4160, 150
|
||
draw.rounded_rectangle((note_x, note_y, note_x + note_w, note_y + note_h), radius=30, fill="#FFF1F3", outline="#C01048", width=5)
|
||
note = "关键注意:入口层缺少统一鉴权和白名单;客户端可通过 Type + Name/Param 触发 SQL 或存储过程能力,异常多数不记录日志。"
|
||
draw_multiline_center(note, note_x + note_w / 2, note_y + note_h / 2, note_w - 120, font_note, fill="#7A271A", line_gap=8)
|
||
|
||
end_x, end_y = W - 560, H - 125
|
||
draw_box(end_x, end_y, 420, 95, "请求结束", "#E8F1FF", "#2864B4", font=font_box_small, radius=45)
|
||
arrow([(resp_x + resp_w / 2, resp_y + resp_h), (resp_x + resp_w / 2, H - 125), (end_x - 210, H - 125)], color="#344054")
|
||
|
||
footer = "源文件:working/MESCommonBase流程图.mmd PNG:working/MESCommonBase流程图.png"
|
||
fw, _ = text_size(footer, font_edge)
|
||
draw.text(((W - fw) / 2, H - 45), footer, font=font_edge, fill="#667085")
|
||
|
||
|
||
draw_title()
|
||
draw_top_flow()
|
||
panel_info = draw_branch_panel()
|
||
resp_panel = draw_detail_panels(panel_info)
|
||
draw_note_and_end(resp_panel)
|
||
|
||
img.save(OUT, format="PNG", dpi=(220, 220), optimize=True)
|
||
print(OUT)
|
||
print(f"{W}x{H}")
|