引言
在图形设计和web开发中,矢量路径数据的高效存储与传输至关重要。本文将通过一个python示例,展示如何将复杂的矢量路径命令序列压缩为json格式,再将其解压还原,并通过matplotlib进行可视化。这一过程可应用于字体设计、矢量图形编辑或web应用中的路径数据传输。
核心功能概述
1. 路径命令解析
- 输入:包含
moveto
、lineto
、qcurveto
(二次贝塞尔曲线)、closepath
命令的路径数据。 - 输出:转换为
matplotlib.path.path
对象,用于绘制矢量图形。
2. 路径数据压缩
- 将路径命令序列转换为紧凑的json格式,便于存储或传输。
- 示例:
moveto((100, 177))
→{"m":[100,177]}
。
3. 路径数据解压
- 将json格式还原为原始路径命令序列,确保数据完整性。
4. 可视化
- 使用
matplotlib
渲染路径,验证压缩/解压过程的正确性。
代码实现详解
1. 路径命令解析(parse_commands函数)
def parse_commands(data): codes = [] vertices = [] for cmd, params in data: if cmd == 'moveto': codes.append(path.moveto) vertices.append(params[0]) elif cmd == 'lineto': codes.append(path.lineto) vertices.append(params[0]) elif cmd == 'qcurveto': # 处理二次贝塞尔曲线(每段需要两个控制点和一个终点) for i in range(0, len(params), 2): control = params[i] end = params[i+1] if i+1 < len(params) else params[-1] codes.extend([path.curve3, path.curve3]) vertices.extend([control, end]) elif cmd == 'closepath': codes.append(path.closepoly) vertices.append(vertices[0]) # 闭合路径回到起点 return codes, vertices
关键点:
- 二次贝塞尔曲线:
qcurveto
命令需两个控制点和一个终点,通过path.curve3
实现。 - 闭合路径:
closepoly
命令自动连接最后一个点到起点。
2. 路径数据压缩(compress_path_to_json函数)
def compress_path_to_json(data): command_map = {'moveto': 'm', 'lineto': 'l', 'qcurveto': 'q', 'closepath': 'z'} compressed = [] for cmd, params in data: cmd_short = command_map[cmd] points = [] if cmd == 'closepath': compressed.append({cmd_short: []}) else: # 将坐标元组展平为一维列表(如 [(x,y), (a,b)] → [x,y,a,b]) for coord in params: points.extend(list(coord)) compressed.append({cmd_short: points}) return json.dumps(compressed, separators=(',', ':'))
示例输出:
[{"m":[100,177]},{"l":[107,169]},{"q":[116,172,127,172]},...]
3. 路径数据解压(decompress_json_to_path函数)
def decompress_json_to_path(compressed_json): command_map = {'m': 'moveto', 'l': 'lineto', 'q': 'qcurveto', 'z': 'closepath'} data = json.loads(compressed_json) decompressed = [] for item in data: cmd_short = next(iter(item)) points = item[cmd_short] cmd = command_map[cmd_short] if not points: decompressed.append((cmd, ())) # 闭合路径无参数 else: # 将一维列表转换为坐标元组(如 [x,y,a,b] → [(x,y), (a,b)]) coords = [] for i in range(0, len(points), 2): coords.append((points[i], points[i+1])) decompressed.append((cmd, tuple(coords))) return decompressed
4. 可视化渲染(show_ttf函数)
def show_ttf(data): codes, vertices = parse_commands(data) path = path(vertices, codes) fig, ax = plt.subplots() patch = patches.pathpatch(path, facecolor='orange', lw=2) ax.add_patch(patch) ax.set_xlim(0, 250) # 根据数据范围调整坐标轴 ax.set_ylim(-30, 220) plt.gca().set_aspect('equal') plt.show()
完整代码与运行结果
示例数据
data = [ ('moveto', ((100, 177),)), ('lineto', ((107, 169),)), ('qcurveto', ((116, 172), (127, 172))), # ... 其他路径命令(如闭合路径、复杂曲线) ]
执行流程
# 压缩数据 compressed_json = compress_path_to_json(data) print("压缩后的json:", compressed_json) # 解压数据 decompressed = decompress_json_to_path(compressed_json) print("解压后的路径数据:", decompressed) # 可视化 show_ttf(decompressed)
结果展示
1. 压缩后的json片段
[ {"m":[100,177]}, {"l":[107,169]}, {"q":[116,172,127,172]}, {"z":[]} ]
2. 解压后的路径数据
[ ('moveto', ((100, 177),)), ('lineto', ((107, 169),)), ('qcurveto', ((116, 172), (127, 172))), ('closepath', ()) ]
技术要点总结
路径命令映射:
m
→moveto
:移动到起点l
→lineto
:绘制直线q
→qcurveto
:二次贝塞尔曲线z
→closepath
:闭合路径
json压缩策略:
- 将坐标元组展平为一维列表,减少冗余。
- 闭合路径(
z
)的参数为空列表。
matplotlib路径渲染:
- 使用
path
对象和pathpatch
实现复杂曲线的绘制。 curve3
命令需成对使用,适配二次贝塞尔曲线的参数。
- 使用
应用场景
- web开发:将矢量路径数据嵌入svg或canvas元素。
- 字体设计:存储和传输字体轮廓路径。
- 数据可视化:动态生成并传输图表路径数据。
import matplotlib.pyplot as plt from matplotlib.path import path import matplotlib.patches as patches # 解析输入数据 def parse_commands(data): codes = [] vertices = [] for command, params in data: if command == 'moveto': codes.append(path.moveto) vertices.append(params[0]) elif command == 'lineto': codes.append(path.lineto) vertices.append(params[0]) elif command == 'qcurveto': # check if there are enough points to form a quadratic bezier curve segment for i in range(0, len(params) - 1, 2): # ensure we don't go out of bounds control_point = params[i] end_point = params[i + 1] codes.extend([path.curve3, path.curve3]) # two curve3 commands for the quad bezier vertices.extend([control_point, end_point]) elif command == 'closepath': codes.append(path.closepoly) vertices.append(vertices[0]) # closing back to the start point return codes, vertices def show_ttf(): codes, vertices = parse_commands(data) path = path(vertices, codes) fig, ax = plt.subplots() patch = patches.pathpatch(path, facecolor='orange', lw=2) ax.add_patch(patch) ax.set_xlim(0, 250) # adjust these limits based on your data's extent ax.set_ylim(-30, 220) # adjust these limits based on your data's extent plt.gca().set_aspect('equal', adjustable='box') # keep aspect ratio equal plt.show() import json def compress_path_to_json(data): command_map = { 'moveto': 'm', 'lineto': 'l', 'qcurveto': 'q', 'closepath': 'z' } compressed = [] for cmd, params in data: command_type = command_map[cmd] points = [] if cmd == 'closepath': pass # closepath无需坐标 else: # 确保params[0]是坐标点列表(即使只有一个点) for param in params: points += list(param) compressed.append({ command_type: points }) return json.dumps(compressed, separators=(',', ':')) data = [('moveto', ((100, 177),)), ('lineto', ((107, 169),)), ('qcurveto', ((116, 172), (127, 172))), ('lineto', ((240, 172),)), ('lineto', ((224, 190),)), ('lineto', ((212, 177),)), ('lineto', ((175, 177),)), ('qcurveto', ((183, 186), (176, 200), (154, 210))), ('lineto', ((152, 207),)), ('qcurveto', ((164, 190), (166, 177))), ('closepath', ()), ('moveto', ((204, 143),)), ('lineto', ((211, 148),)), ('lineto', ((198, 162),)), ('lineto', ((189, 152),)), ('lineto', ((143, 152),)), ('lineto', ((128, 160),)), ('qcurveto', ((129, 149), (129, 116), (128, 102))), ('lineto', ((142, 106),)), ('lineto', ((142, 114),)), ('lineto', ((191, 114),)), ('lineto', ((191, 105),)), ('lineto', ((205, 111),)), ('qcurveto', ((204, 119), (204, 135), (204, 143))), ('closepath', ()), ('moveto', ((142, 147),)), ('lineto', ((191, 147),)), ('lineto', ((191, 119),)), ('lineto', ((142, 119),)), ('closepath', ()), ('moveto', ((119, 87),)), ('lineto', ((218, 87),)), ('lineto', ((218, 6),)), ('qcurveto', ((218, -3), (210, -5), (181, -3))), ('lineto', ((181, -8),)), ('qcurveto', ((212, -13), (212, -26))), ('qcurveto', ((221, -22), (231, -12), (231, 2))), ('lineto', ((231, 80),)), ('lineto', ((240, 87),)), ('lineto', ((224, 102),)), ('lineto', ((216, 92),)), ('lineto', ((119, 92),)), ('lineto', ((105, 100),)), ('qcurveto', ((106, 84), (106, 5), (105, -26))), ('lineto', ((119, -18),)), ('closepath', ()), ('moveto', ((196, 58),)), ('lineto', ((203, 63),)), ('lineto', ((188, 76),)), ('lineto', ((182, 67),)), ('lineto', ((151, 67),)), ('lineto', ((137, 76),)), ('qcurveto', ((138, 59), (138, 30), (137, 5))), ('lineto', ((150, 11),)), ('lineto', ((150, 21),)), ('lineto', ((184, 21),)), ('lineto', ((184, 10),)), ('lineto', ((197, 16),)), ('qcurveto', ((196, 27), (196, 48), (196, 58))), ('closepath', ()), ('moveto', ((150, 62),)), ('lineto', ((184, 62),)), ('lineto', ((184, 26),)), ('lineto', ((150, 26),)), ('closepath', ()), ('moveto', ((36, 63),)), ('qcurveto', ((66, 100), (94, 148))), ('lineto', ((103, 152),)), ('lineto', ((83, 163),)), ('qcurveto', ((74, 138), (66, 125))), ('lineto', ((30, 123),)), ('qcurveto', ((50, 154), (71, 193))), ('lineto', ((82, 197),)), ('lineto', ((59, 209),)), ('qcurveto', ((51, 178), (23, 124), (14, 124))), ('lineto', ((25, 106),)), ('qcurveto', ((31, 111), (50, 117), (63, 119))), ('qcurveto', ((44, 87), (24, 63), (18, 62))), ('lineto', ((28, 44),)), ('qcurveto', ((39, 51), (68, 60), (98, 66))), ('lineto', ((97, 70),)), ('qcurveto', ((67, 66), (36, 63))), ('closepath', ()), ('moveto', ((11, 14),)), ('lineto', ((21, -4),)), ('qcurveto', ((30, 4), (65, 20), (95, 30))), ('lineto', ((94, 34),)), ('qcurveto', ((72, 28), (25, 16), (11, 14))), ('closepath', ())] def decompress_json_to_path(compressed_json): command_map = { 'm': 'moveto', 'l': 'lineto', 'q': 'qcurveto', 'z': 'closepath' } data = json.loads(compressed_json) decompressed = [] for item in data: cmd_char = next(iter(item)) # 获取命令字符 points = item[cmd_char] original_cmd = command_map[cmd_char] if not points: # closepath,参数为空 decompressed.append((original_cmd, ())) else: # 将points列表转换为坐标点元组的元组 tuples = [] for i in range(0, len(points), 2): x = points[i] y = points[i + 1] tuples.append((x, y)) params = tuple(tuples) decompressed.append((original_cmd, params)) return decompressed compressed_json = compress_path_to_json(data) # 解压 decompressed = decompress_json_to_path(compressed_json)
以上就是使用python实现矢量路径的压缩、解压与可视化的详细内容,更多关于python矢量路径解压缩与可视化的资料请关注代码网其它相关文章!
发表评论