如何用Python缩放SVG矢量图,实现Inkscape手写字体自动化生成?
SVG字符路径的缩放与平移自动化实现
针对你从Remarkable2导出的手写字符SVG,需要将其批量缩放并平移到目标位置的需求,以下是具体的数学逻辑和Python实现方案:
核心变换逻辑
SVG坐标系以左上角为原点,Y轴向下延伸。要将原始字符的 bounding box(X:167.996, Y:86.660, W:32.623, H:38.221)映射到目标区域(X:126, Y:338, W:504, H:620),需按以下步骤计算坐标变换:
计算缩放因子:分别基于宽度和高度计算缩放比例
- 宽度缩放因子:
scale_x = 目标宽度 / 原始宽度 = 504 / 32.623 ≈ 15.45 - 高度缩放因子:
scale_y = 目标高度 / 原始高度 = 620 / 38.221 ≈ 16.22
- 宽度缩放因子:
计算坐标平移:先将原始字符的左上角对齐到原点,缩放后再平移到目标位置。对任意原始坐标点
(x, y),变换公式为:- 变换后X坐标:
x' = (x - 原始X) * scale_x + 目标X - 变换后Y坐标:
y' = (y - 原始Y) * scale_y + 目标Y
- 变换后X坐标:
Python代码实现
使用内置的xml.etree.ElementTree解析SVG,处理路径的d属性,同时兼容绝对/相对坐标指令:
import xml.etree.ElementTree as ET import re def transform_path_d(d_str, orig_x, orig_y, orig_w, orig_h, dest_x, dest_y, dest_w, dest_h): # 计算缩放比例 scale_x = dest_w / orig_w scale_y = dest_h / orig_h # 拆分路径指令与坐标 pattern = re.compile(r'([a-zA-Z])([^a-zA-Z]*)') path_parts = pattern.findall(d_str) transformed_parts = [] current_abs_x = 0 current_abs_y = 0 for cmd, coord_str in path_parts: # 提取坐标数值 coords = list(map(float, re.findall(r'-?\d+\.?\d*', coord_str))) new_coords = [] # 按坐标对处理(适配M/L/C等指令) for i in range(0, len(coords), 2): x = coords[i] y = coords[i+1] if i+1 < len(coords) else 0 if cmd.isupper(): # 绝对坐标直接变换 trans_x = (x - orig_x) * scale_x + dest_x trans_y = (y - orig_y) * scale_y + dest_y current_abs_x, current_abs_y = trans_x, trans_y else: # 相对坐标先转绝对,变换后再转回相对 abs_x = current_abs_x + x abs_y = current_abs_y + y trans_abs_x = (abs_x - orig_x) * scale_x + dest_x trans_abs_y = (abs_y - orig_y) * scale_y + dest_y trans_x = trans_abs_x - current_abs_x trans_y = trans_abs_y - current_abs_y current_abs_x, current_abs_y = trans_abs_x, trans_abs_y new_coords.append(f"{trans_x:.3f}") # 保留3位小数精简路径 new_coords.append(f"{trans_y:.3f}") transformed_parts.append(f"{cmd} {' '.join(new_coords)}") return ' '.join(transformed_parts) def process_single_svg(input_svg, output_svg, orig_bbox, dest_bbox): # 解析原始与目标bounding box参数 orig_x, orig_y, orig_w, orig_h = orig_bbox dest_x, dest_y, dest_w, dest_h = dest_bbox tree = ET.parse(input_svg) root = tree.getroot() ns = {'svg': 'http://www.w3.org/2000/svg'} # SVG命名空间 # 遍历所有路径(假设已移除图层,路径在根目录) for path in root.findall('.//svg:path', ns): orig_d = path.get('d') if orig_d: new_d = transform_path_d(orig_d, orig_x, orig_y, orig_w, orig_h, dest_x, dest_y, dest_w, dest_h) path.set('d', new_d) # 保存变换后的SVG tree.write(output_svg, encoding='utf-8', xml_declaration=True) # 示例:处理字母A的SVG process_single_svg( input_svg='a_original.svg', output_svg='a_transformed.svg', orig_bbox=(167.996, 86.660, 32.623, 38.221), dest_bbox=(126, 338, 504, 620) )
批量处理优化
如果每个字符的原始bounding box不同,可以用svgpathtools自动获取每个SVG的路径边界,无需手动输入参数:
from svgpathtools import svg2paths def get_svg_bbox(svg_path): paths, _ = svg2paths(svg_path) min_x, min_y, max_x, max_y = paths[0].bbox() return (min_x, min_y, max_x - min_x, max_y - min_y) # 批量处理示例(遍历所有字符SVG) import os input_dir = 'raw_chars' output_dir = 'transformed_chars' os.makedirs(output_dir, exist_ok=True) dest_bbox = (126, 338, 504, 620) for filename in os.listdir(input_dir): if filename.endswith('.svg'): input_path = os.path.join(input_dir, filename) output_path = os.path.join(output_dir, filename) orig_bbox = get_svg_bbox(input_path) process_single_svg(input_path, output_path, orig_bbox, dest_bbox)
注意事项
- 命名空间处理:SVG文件通常包含
xmlns属性,必须在查找路径时指定命名空间,否则无法匹配<path>标签 - 特殊指令支持:上述代码适配了大部分常用路径指令(M/L/C/Q等),若遇到单坐标指令(H/V/h/v),需额外补充逻辑处理
- 精度控制:通过
:.3f保留三位小数,避免路径字符串过于冗长,同时保证视觉精度
内容的提问来源于stack exchange,提问作者Joshua Ritchie
相关产品推荐
相关产品推荐

