# _*_ coding:utf-8 _*_
#
# @Version : 1.1
# @Project : https://github.com/shueho/BioDataTools
# @Time : 2025/7/23 20:00
# @Update : 2025/8/13 14:00
# @Author : Hao Xue
# @E-mail : studid@163.com
# @File : TrnaStructureBeautifier.py
#
# Enhancement of tRNA secondary structure diagrams generated using the ViennaRNA package.
import os
import re
import argparse
# old = sys.argv[1]
def parse_arguments():
parser = argparse.ArgumentParser(description="RNA结构图美化参数配置")
# === 输入控制 ===
parser.add_argument(
"-i", "--input",
type=str,
required=True,
help="SVG文件或者SVG文件存放的文件夹路径"
)
# === 基础布局 ===
parser.add_argument(
"-s", "--size-weight",
type=float,
default=1.4,
help="图形缩放比例(默认 1.4)"
)
parser.add_argument(
"-p", "--per-row",
type=int,
default=4,
help="每行图片数量(默认 4)"
)
parser.add_argument(
"-hg", "--horizontal-gap",
type=int,
default=8,
help="图片水平间隔(默认 8)"
)
parser.add_argument(
"-vg", "--vertical-gap",
type=int,
default=5,
help="图片垂直间隔(默认 5)"
)
# === 碱基连线颜色 ===
parser.add_argument(
"-ac", "--adjacent-color",
type=str,
default="blue",
help='相邻碱基连线颜色(支持名称或 HEX,如 "blue" 或 "#00FF00",默认 "blue")'
)
parser.add_argument(
"-pc", "--pair-color",
type=str,
default="red",
help='配对碱基连线颜色(支持名称或 HEX,如 "red" 或 "#FF0000",默认 "red")'
)
# === 碱基圆圈样式 ===
parser.add_argument(
"-bf", "--base-fill",
type=str,
default="white",
help='碱基圆圈填充色(默认 "white")'
)
parser.add_argument(
"-bs", "--base-stroke",
type=str,
default="black",
help='碱基圆圈轮廓色(默认 "black")'
)
# === 碱基美化图 ===
parser.add_argument(
"-A", "--base-a",
type=str,
default="red",
help='美化A碱基圆圈填充色(默认 "red")'
)
parser.add_argument(
"-U", "--base-u",
type=str,
default="blue",
help='美化U/T碱基圆圈填充色(默认 "blue")'
)
parser.add_argument(
"-G", "--base-g",
type=str,
default="green",
help='美化G碱基圆圈填充色(默认 "green")'
)
parser.add_argument(
"-C", "--base-c",
type=str,
default="yellow",
help='美化C碱基圆圈填充色(默认 "yellow")'
)
# === 反密码子控制 ===
# parser.add_argument(
# "-af", "--anticodon-file",
# type=str,
# default=None,
# help="反密码子位点文件路径(文本文件,每行一个位置)"
# )
# parser.add_argument(
# "-aF", "--anti-fill",
# type=str,
# default="red",
# help='反密码子圆圈填充色(默认 "red")'
# )
# parser.add_argument(
# "-aS", "--anti-stroke",
# type=str,
# default="black",
# help='反密码子圆圈轮廓色(默认 "black")'
# )
args = parser.parse_args()
return args
args = parse_arguments()
base_color = {"A": args.base_a, "U": args.base_u, "G": args.base_g, "C": args.base_c, }
# 设置目标文件夹名称
modified_folder = "modified"
# 创建文件夹(如果已存在则忽略)
os.makedirs(modified_folder, exist_ok=True)
print("已创建文件夹: modified")
def read_svg(spath):
with open(spath) as f:
return f.read()
def set_new_text(x_y_char, fill_=args.base_fill, stroke_=args.base_stroke):
x = x_y_char[0]
y = x_y_char[1]
t = x_y_char[2].upper().replace("T", "U")
# 核心修改:保持圆圈半径r=5不变,调整dy=0使字母在垂直方向也居中(结合text-anchor=middle实现正中心)
return '\n {}\n'.format(
x, y, fill_, stroke_, x, y, t)
# r=圆圈半径,dy=字体垂直位移,font-size=字体大小
def set_new_lines(x_y_char_1, x_y_char_2, stroke_):
x1, y1, _ = x_y_char_1
x2, y2, _ = x_y_char_2
xy = (x1, y1, x2, y2)
return '\n'.format(*xy, stroke_)
# 保留原有 get_add_line 函数,不修改(保持代码秩序)
def get_add_line(svg_text):
# pattern = r']*?y1="([^"]+)"[^>]*?x2="([^"]+)"[^>]*?y2="([^"]+)"'
pattern = r']*\s*x1="([^"]+)"[^>]*\s*y1="([^"]+)"[^>]*\s*x2="([^"]+)"[^>]*\s*y2="([^"]+)"'
matches = re.findall(pattern, svg_text)
result = []
for x1_str, y1_str, x2_str, y2_str in matches:
try:
x1 = float(x1_str)
y1 = float(y1_str)
x2 = float(x2_str)
y2 = float(y2_str)
result.append(set_new_lines((x1, y1, ""), (x2, y2, ""), args.pair_color))
except ValueError:
# 如果坐标无法转换为 float,跳过该元素
continue
return "".join(result)
# 核心修改1:extract_text_info 补充返回坐标边界(用于居中计算),生成内容逻辑完全不变
def extract_text_info(svg_text):
# 正则表达式匹配 标签中的 x、y 和字符内容
# pattern = r']*>([^<]+)'
pattern = r']*\s*x="([^"]+)"[^>]*\s*y="([^"]+)"[^>]*>(.*?)<\/text>'
matches = re.findall(pattern, svg_text)
result = []
for x_str, y_str, char in matches:
try:
x = float(x_str)
y = float(y_str)
result.append((x, y, char))
except ValueError:
# 如果 x 或 y 无法转换为 float,跳过该元素
continue
# 计算坐标边界(新增:用于后续居中)
if not result:
min_x = max_x = min_y = max_y = 0
else:
xs = [p[0] for p in result]
ys = [p[1] for p in result]
min_x, max_x = min(xs), max(xs)
min_y, max_y = min(ys), max(ys)
# 原有生成内容逻辑完全不变(保证字母在圆圈内)
content = '\n'
content += get_add_line(svg_text)
for i in range(1, len(result)):
# content += set_new_text(result[i])
content += set_new_lines(result[i - 1], result[i], args.adjacent_color)
content += set_new_text(result[i - 1])
content += set_new_text(result[len(result) - 1]) + '\n'
# 新增返回坐标边界,用于居中计算
return content, min_x, max_x, min_y, max_y
def save_svg(content, outpath):
with open(outpath, "w") as f:
f.write(content)
# 核心修改2:modi_svg 改为超大画布 + 自动居中,保留原有逻辑框架
def modi_svg(path):
t = read_svg(path)
name_ = os.path.basename(path)
if "-" in name_:
name_ = name_.split("-")[0]
else:
name_ = name_.split(".")[0]
# 提取内容 + 坐标边界(新增)
mid_, min_x, max_x, min_y, max_y = extract_text_info(t)
scale = args.size_weight
# 1. 超大画布(2000×2000,可自行调大)
canvas_w = 2000
canvas_h = 2000
# 2. 计算结构中心(用于居中)
if min_x == max_x and min_y == max_y:
cx, cy = 0, 0
else:
cx = (min_x + max_x) / 2 # 结构水平中心
cy = (min_y + max_y) / 2 # 结构垂直中心
# 3. 计算居中偏移量(保证结构在画布正中间)
translate_x = (canvas_w / 2) / scale - cx # 水平居中
translate_y = (canvas_h / 2) / scale - cy # 垂直居中
# 保留原有 head_ 结构,仅替换画布尺寸和偏移量,同时修改名称位置避免重叠
head_ = '''
'
save_svg(head_ + mid_ + tail_, "modified/modified_" + name_ + ".svg")
return name_
# save_svg(modi_svg(old), "new.svg")
def remove_xml_declaration(content):
"""Remove XML declaration from SVG content."""
return re.sub(r'<\?xml[^>]*\?>\s*', '', content)
def extract_dimensions(svg_content):
"""Extract width and height from SVG content."""
width_match = re.search(r'width="([^"]+)"', svg_content)
height_match = re.search(r'height="([^"]+)"', svg_content)
if not width_match or not height_match:
raise ValueError("Could not extract width or height")
return width_match.group(1), height_match.group(1)
def merge_horizontal_group(svg_files, output_file):
"""Merge SVGs in a group horizontally."""
svgs = []
total_width = 0
max_height = 0
gap = args.horizontal_gap
for filename in svg_files:
with open(filename, 'r') as f:
content = f.read()
content = remove_xml_declaration(content)
width, height = extract_dimensions(content)
try:
width_px = float(width.strip('px'))
height_px = float(height.strip('px'))
except ValueError:
raise ValueError(f"Invalid unit in file {filename}")
svgs.append({
'filename': filename,
'content': content,
'width': width,
'height': height,
'width_px': width_px,
'height_px': height_px
})
total_width += sum(svg_info['width_px'] for svg_info in svgs) + gap * (len(svgs) - 1)
max_height = max(max_height, height_px)
new_svg = [
'',
'')
with open(output_file, 'w') as f:
f.write('\n'.join(new_svg))
def merge_vertical_groups(group_files, output_file):
"""Merge all horizontal groups vertically."""
svgs = []
max_width = 0
total_height = 0
gap = args.vertical_gap
for filename in group_files:
with open(filename, 'r') as f:
content = f.read()
content = remove_xml_declaration(content)
width, height = extract_dimensions(content)
try:
width_px = float(width.strip('px'))
height_px = float(height.strip('px'))
except ValueError:
raise ValueError(f"Invalid unit in file {filename}")
svgs.append({
'filename': filename,
'content': content,
'width': width,
'height': height,
'width_px': width_px,
'height_px': height_px
})
max_width = max(max_width, width_px)
total_height += sum(svg_info['height_px'] for svg_info in svgs) + gap * (len(svgs) - 1)
new_svg = [
'',
''.format(max_width, total_height)
]
current_y = 0.0
for svg_info in svgs:
inner_content = re.sub(r']*>', '', svg_info['content'])
inner_content = re.sub(r'', '', inner_content)
new_svg.append(''.format(current_y))
new_svg.append(inner_content)
new_svg.append('')
current_y += svg_info['height_px'] + gap
new_svg.append('')
with open(output_file, 'w') as f:
f.write('\n'.join(new_svg))
def update_circle_colors(svg_content, base_color_map):
new_ = ""
ls = svg_content.split("\n")
for i in range(len(ls)):
if ")[A-Z](?=<\/text>)', ls[i + 2])[0]
if flag not in base_color_map:
new_ += ls[i] + "\n"
continue
new_ += ls[i].replace(args.base_fill, base_color_map[flag]) + "\n"
else:
new_ += ls[i] + "\n"
return new_
if __name__ == "__main__":
# 示例调用
print("参数解析结果:")
# print(f"主数据文件: {args.input}")
print(f"大小权重: {args.size_weight}")
print(f"每行图片数量: {args.per_row}")
print(f"图片水平间隔: {args.horizontal_gap}")
print(f"图片垂直间隔: {args.vertical_gap}")
print(f"相邻碱基连线颜色: {args.adjacent_color}")
print(f"配对碱基连线颜色: {args.pair_color}")
print(f"碱基填充色: {args.base_fill}")
print(f"碱基轮廓色: {args.base_stroke}")
# print(f"反密码子文件: {args.anticodon_file}")
# print(f"反密码子填充色: {args.anti_fill}")
# print(f"反密码子轮廓色: {args.anti_stroke}")
# 如果是文件,直接添加到列表
file_list = []
group_size = args.per_row
if os.path.isfile(args.input):
file_list.append(args.input)
print(f"已添加文件: {args.input}")
# 如果是文件夹,添加文件夹内所有文件(不递归子文件夹)
elif os.path.isdir(args.input):
for filename in os.listdir(args.input):
file_path = os.path.join(args.input, filename)
if os.path.isfile(file_path) and "modified_" not in file_path and ".svg" == file_path[-4:]: # 只处理文件
file_list.append(file_path)
file_list.sort()
print(f"已添加文件夹内所有文件: {args.input}")
modi_files = []
for i in file_list:
n = modi_svg(i)
modi_files.append("modified/modified_" + n + ".svg")
# Step 2: Merge into horizontal groups
group_index = 0
group_files = []
for i in range(0, len(modi_files), group_size):
group = modi_files[i:i + group_size]
if not group:
break
group_output = f"modified/group_{group_index}.svg"
merge_horizontal_group(group, str(group_output))
group_files.append(group_output)
group_index += 1
# Step 3: Merge all groups vertically
final_output = "modified/final.svg"
merge_vertical_groups(group_files, str(final_output))
print(f"未上色SVG保存到: {final_output}")
print(f"美化A碱基填充色: {args.base_a}")
print(f"美化U碱基填充色: {args.base_u}")
print(f"美化G碱基填充色: {args.base_g}")
print(f"美化C碱基填充色: {args.base_c}")
fisvg = read_svg(str(final_output))
cosvg = update_circle_colors(fisvg, base_color)
save_svg(cosvg, "modified/final_color.svg")
# 核心修改3:修复打印路径错误(原写成 final.svg,改为 final_color.svg)
print(f"上色SVG保存到: modified/final_color.svg")
# print(modi_files)